Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-msal for openSUSE:Factory 
checked in at 2021-07-01 07:05:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-msal (Old)
 and      /work/SRC/openSUSE:Factory/.python-msal.new.2625 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-msal"

Thu Jul  1 07:05:40 2021 rev:8 rq:903198 version:1.12.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes  2021-04-12 
15:49:43.449261928 +0200
+++ /work/SRC/openSUSE:Factory/.python-msal.new.2625/python-msal.changes        
2021-07-01 07:05:55.447341313 +0200
@@ -1,0 +2,28 @@
+Tue Jun 29 12:40:07 UTC 2021 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to version 1.12.0
+  + New feature: MSAL Python supports ConfidentialClientApplication(..., 
azure_region=...).
+    If your app is deployed in Azure, you can use this new feature to pin a 
region.
+    (#295, #358)
+  + New feature: Historically MSAL Python attempts to acquire a Refresh Token 
(RT) by
+    default. Since this version, MSAL Python supports 
ConfidentialClientApplication(...,
+    excluse_scopes=["offline_access"]) to opt out of RT (#207, #361)
+  + Improvement: acquire_token_interactive(...) can also trigger browser when
+    running inside WSL (8d86917)
+  + Adjustment: get_accounts(...) would automatically combine equivalent 
accounts,
+    so that your account selector widget could be easier to use (#349)
+  + Document: MSAL Python has long been accepting 
acquire_token_interactive(..., prompt="create"),
+    now we officially documented it. (#356, #360)
+- from version 1.11.0
+  + Enhancement: ConfidentialClientApplication also supports
+    acquire_token_by_username_password() now. (#294, #344)
+  + Enhancement: PublicClientApplication's acquire_token_interactive() also 
supports WSL Ubuntu
+    18.04 (#332, #333)
+  + Enhancement: Enable a retry once behavior on connection error. (But this 
is only available
+    from the default http client. If your app supplies your customized 
http_client via MSAL
+    constructors, it is your http_client's job to decide whether retry.) (#326)
+  + Enhancement: MSAL improves the internal telemetry mechanism. (#137, #175, 
#329, #345)
+  + Bugfix: Better compatibility on handling SAML token when using
+    acquire_token_by_username_password() with ADFS. (#336)
+
+-------------------------------------------------------------------

Old:
----
  msal-1.10.0.tar.gz

New:
----
  msal-1.12.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-msal.spec ++++++
--- /var/tmp/diff_new_pack.RSVMx7/_old  2021-07-01 07:05:55.879337938 +0200
+++ /var/tmp/diff_new_pack.RSVMx7/_new  2021-07-01 07:05:55.879337938 +0200
@@ -21,7 +21,7 @@
 %define skip_python2 1
 %endif
 Name:           python-msal
-Version:        1.10.0
+Version:        1.12.0
 Release:        0
 Summary:        Microsoft Authentication Library (MSAL) for Python
 License:        MIT

++++++ msal-1.10.0.tar.gz -> msal-1.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/LICENSE new/msal-1.12.0/LICENSE
--- old/msal-1.10.0/LICENSE     1970-01-01 01:00:00.000000000 +0100
+++ new/msal-1.12.0/LICENSE     2021-05-19 22:28:05.000000000 +0200
@@ -0,0 +1,24 @@
+The MIT License (MIT)
+
+Copyright (c) Microsoft Corporation. 
+All rights reserved.
+
+This code is licensed under the MIT License.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files(the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions :
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/PKG-INFO new/msal-1.12.0/PKG-INFO
--- old/msal-1.10.0/PKG-INFO    2021-03-08 21:46:32.805985200 +0100
+++ new/msal-1.12.0/PKG-INFO    2021-05-19 22:28:13.778312400 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: msal
-Version: 1.10.0
+Version: 1.12.0
 Summary: The Microsoft Authentication Library (MSAL) for Python library 
enables your app to access the Microsoft Cloud by supporting authentication of 
users with Microsoft Azure Active Directory accounts (AAD) and Microsoft 
Accounts (MSA) using industry standard OAuth2 and OpenID Connect.
 Home-page: 
https://github.com/AzureAD/microsoft-authentication-library-for-python
 Author: Microsoft Corporation
@@ -21,8 +21,14 @@
         
         Quick links:
         
-        | [Getting 
Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp)
 | 
[Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki)
 | [Samples](https://aka.ms/aaddevsamplesv2) | 
[Support](README.md#community-help-and-support)
-        | --- | --- | --- | --- |
+        | [Getting 
Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp)
 | 
[Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki)
 | [Samples](https://aka.ms/aaddevsamplesv2) | 
[Support](README.md#community-help-and-support) | 
[Feedback](https://forms.office.com/r/TMjZkDbzjY) |
+        | --- | --- | --- | --- | --- |
+        
+        ## Scenarios supported
+        
+        Click on the following thumbnail to visit a large map with clickable 
links to proper samples.
+        
+        [![Map effect won't work inside github's markdown file, so we have to 
use a thumbnail here to lure audience to a real static 
website](docs/thumbnail.png)](https://msal-python.readthedocs.io/en/latest/)
         
         ## Installation
         
@@ -135,6 +141,9 @@
         Here is the latest Q&A on Stack Overflow for MSAL:
         
[http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal)
         
+        ## Submit Feedback
+        We'd like your thoughts on this library. Please complete [this short 
survey.](https://forms.office.com/r/TMjZkDbzjY)
+        
         ## Security Reporting
         
         If you find a security issue with our libraries or services please 
report it to [sec...@microsoft.com](mailto:sec...@microsoft.com) with as much 
detail as possible. Your submission may be eligible for a bounty through the 
[Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post 
security issues to GitHub Issues or any other public site. We will contact you 
shortly upon receiving the information. We encourage you to get notifications 
of when security incidents occur by visiting [this 
page](https://technet.microsoft.com/security/dd252948) and subscribing to 
Security Advisory Alerts.
@@ -153,11 +162,11 @@
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: OS Independent
 Description-Content-Type: text/markdown
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/README.md new/msal-1.12.0/README.md
--- old/msal-1.10.0/README.md   2021-03-08 21:46:19.000000000 +0100
+++ new/msal-1.12.0/README.md   2021-05-19 22:28:05.000000000 +0200
@@ -12,8 +12,14 @@
 
 Quick links:
 
-| [Getting 
Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp)
 | 
[Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki)
 | [Samples](https://aka.ms/aaddevsamplesv2) | 
[Support](README.md#community-help-and-support)
-| --- | --- | --- | --- |
+| [Getting 
Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp)
 | 
[Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki)
 | [Samples](https://aka.ms/aaddevsamplesv2) | 
[Support](README.md#community-help-and-support) | 
[Feedback](https://forms.office.com/r/TMjZkDbzjY) |
+| --- | --- | --- | --- | --- |
+
+## Scenarios supported
+
+Click on the following thumbnail to visit a large map with clickable links to 
proper samples.
+
+[![Map effect won't work inside github's markdown file, so we have to use a 
thumbnail here to lure audience to a real static 
website](docs/thumbnail.png)](https://msal-python.readthedocs.io/en/latest/)
 
 ## Installation
 
@@ -126,6 +132,9 @@
 Here is the latest Q&A on Stack Overflow for MSAL:
 
[http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal)
 
+## Submit Feedback
+We'd like your thoughts on this library. Please complete [this short 
survey.](https://forms.office.com/r/TMjZkDbzjY)
+
 ## Security Reporting
 
 If you find a security issue with our libraries or services please report it 
to [sec...@microsoft.com](mailto:sec...@microsoft.com) with as much detail as 
possible. Your submission may be eligible for a bounty through the [Microsoft 
Bounty](http://aka.ms/bugbounty) program. Please do not post security issues to 
GitHub Issues or any other public site. We will contact you shortly upon 
receiving the information. We encourage you to get notifications of when 
security incidents occur by visiting [this 
page](https://technet.microsoft.com/security/dd252948) and subscribing to 
Security Advisory Alerts.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/__init__.py 
new/msal-1.12.0/msal/__init__.py
--- old/msal-1.10.0/msal/__init__.py    2021-03-08 21:46:19.000000000 +0100
+++ new/msal-1.12.0/msal/__init__.py    2021-05-19 22:28:05.000000000 +0200
@@ -31,5 +31,6 @@
     ConfidentialClientApplication,
     PublicClientApplication,
     )
+from .oauth2cli.oidc import Prompt
 from .token_cache import TokenCache, SerializableTokenCache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/application.py 
new/msal-1.12.0/msal/application.py
--- old/msal-1.10.0/msal/application.py 2021-03-08 21:46:19.000000000 +0100
+++ new/msal-1.12.0/msal/application.py 2021-05-19 22:28:05.000000000 +0200
@@ -8,7 +8,7 @@
 import logging
 import sys
 import warnings
-import uuid
+from threading import Lock
 
 import requests
 
@@ -18,52 +18,15 @@
 from .wstrust_request import send_request as wst_send_request
 from .wstrust_response import *
 from .token_cache import TokenCache
+import msal.telemetry
+from .region import _detect_region
 
 
 # The __init__.py will import this. Not the other way around.
-__version__ = "1.10.0"
+__version__ = "1.12.0"
 
 logger = logging.getLogger(__name__)
 
-def decorate_scope(
-        scopes, client_id,
-        reserved_scope=frozenset(['openid', 'profile', 'offline_access'])):
-    if not isinstance(scopes, (list, set, tuple)):
-        raise ValueError("The input scopes should be a list, tuple, or set")
-    scope_set = set(scopes)  # Input scopes is typically a list. Copy it to a 
set.
-    if scope_set & reserved_scope:
-        # These scopes are reserved for the API to provide good experience.
-        # We could make the developer pass these and then if they do they will
-        # come back asking why they don't see refresh token or user 
information.
-        raise ValueError(
-            "API does not accept {} value as user-provided scopes".format(
-                reserved_scope))
-    if client_id in scope_set:
-        if len(scope_set) > 1:
-            # We make developers pass their client id, so that they can express
-            # the intent that they want the token for themselves (their own
-            # app).
-            # If we do not restrict them to passing only client id then they
-            # could write code where they expect an id token but end up getting
-            # access_token.
-            raise ValueError("Client Id can only be provided as a single 
scope")
-        decorated = set(reserved_scope)  # Make a writable copy
-    else:
-        decorated = scope_set | reserved_scope
-    return list(decorated)
-
-CLIENT_REQUEST_ID = 'client-request-id'
-CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry'
-
-def _get_new_correlation_id():
-    correlation_id = str(uuid.uuid4())
-    logger.debug("Generates correlation_id: %s", correlation_id)
-    return correlation_id
-
-
-def _build_current_telemetry_request_header(public_api_id, 
force_refresh=False):
-    return "1|{},{}|".format(public_api_id, "1" if force_refresh else "0")
-
 
 def extract_certs(public_cert_content):
     # Parses raw public certificate file contents and returns a list of strings
@@ -119,6 +82,8 @@
     GET_ACCOUNTS_ID = "902"
     REMOVE_ACCOUNT_ID = "903"
 
+    ATTEMPT_REGION_DISCOVERY = True  # "TryAutoDetect"
+
     def __init__(
             self, client_id,
             client_credential=None, authority=None, validate_authority=True,
@@ -126,12 +91,18 @@
             http_client=None,
             verify=True, proxies=None, timeout=None,
             client_claims=None, app_name=None, app_version=None,
-            client_capabilities=None):
+            client_capabilities=None,
+            azure_region=None,  # Note: We choose to add this param in this 
base class,
+                # despite it is currently only needed by 
ConfidentialClientApplication.
+                # This way, it holds the same positional param place for PCA,
+                # when we would eventually want to add this feature to PCA in 
future.
+            exclude_scopes=None,
+            ):
         """Create an instance of application.
 
         :param str client_id: Your app has a client_id after you register it 
on AAD.
 
-        :param str client_credential:
+        :param Union[str, dict] client_credential:
             For :class:`PublicClientApplication`, you simply use `None` here.
             For :class:`ConfidentialClientApplication`,
             it can be a string containing client secret,
@@ -187,7 +158,12 @@
             By default, an in-memory cache will be created and used.
         :param http_client: (optional)
             Your implementation of abstract class HttpClient 
<msal.oauth2cli.http.http_client>
-            Defaults to a requests session instance
+            Defaults to a requests session instance.
+            Since MSAL 1.11.0, the default session would be configured
+            to attempt one retry on connection error.
+            If you are providing your own http_client,
+            it will be your http_client's duty to decide whether to perform 
retry.
+
         :param verify: (optional)
             It will be passed to the
             `verify parameter in the underlying requests library
@@ -226,11 +202,75 @@
             MSAL will combine them into
             `claims parameter 
<https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter`_
             which you will later provide via one of the acquire-token request.
+
+        :param str azure_region:
+            Added since MSAL Python 1.12.0.
+
+            As of 2021 May, regional service is only available for
+            ``acquire_token_for_client()`` sent by any of the following 
scenarios::
+
+            1. An app powered by a capable MSAL
+               (MSAL Python 1.12+ will be provisioned)
+
+            2. An app with managed identity, which is formerly known as MSI.
+               (However MSAL Python does not support managed identity,
+               so this one does not apply.)
+
+            3. An app authenticated by
+               `Subject Name/Issuer (SNI) 
<https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_.
+
+            4. An app which already onboard to the region's allow-list.
+
+            MSAL's default value is None, which means region behavior remains 
off.
+            If enabled, the `acquire_token_for_client()`-relevant traffic
+            would remain inside that region.
+
+            App developer can opt in to a regional endpoint,
+            by provide its region name, such as "westus", "eastus2".
+            You can find a full list of regions by running
+            ``az account list-locations -o table``, or referencing to
+            `this doc 
<https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.management.resourcemanager.fluent.core.region?view=azure-dotnet>`_.
+
+            An app running inside Azure Functions and Azure VM can use a 
special keyword
+            ``ClientApplication.ATTEMPT_REGION_DISCOVERY`` to auto-detect 
region.
+
+            .. note::
+
+                Setting ``azure_region`` to non-``None`` for an app running
+                outside of Azure Function/VM could hang indefinitely.
+
+                You should consider opting in/out region behavior on-demand,
+                by loading ``azure_region=None`` or ``azure_region="westus"``
+                or ``azure_region=True`` (which means opt-in and auto-detect)
+                from your per-deployment configuration, and then do
+                ``app = ConfidentialClientApplication(..., 
azure_region=azure_region)``.
+
+                Alternatively, you can configure a short timeout,
+                or provide a custom http_client which has a short timeout.
+                That way, the latency would be under your control,
+                but still less performant than opting out of region feature.
+        :param list[str] exclude_scopes: (optional)
+            Historically MSAL hardcodes `offline_access` scope,
+            which would allow your app to have prolonged access to user's data.
+            If that is unnecessary or undesirable for your app,
+            now you can use this parameter to supply an exclusion list of 
scopes,
+            such as ``exclude_scopes = ["offline_access"]``.
         """
         self.client_id = client_id
         self.client_credential = client_credential
         self.client_claims = client_claims
         self._client_capabilities = client_capabilities
+
+        if exclude_scopes and not isinstance(exclude_scopes, list):
+            raise ValueError(
+                "Invalid exclude_scopes={}. It need to be a list of 
strings.".format(
+                repr(exclude_scopes)))
+        self._exclude_scopes = frozenset(exclude_scopes or [])
+        if "openid" in self._exclude_scopes:
+            raise ValueError(
+                'Invalid exclude_scopes={}. You can not opt out "openid" 
scope'.format(
+                repr(exclude_scopes)))
+
         if http_client:
             self.http_client = http_client
         else:
@@ -241,15 +281,101 @@
             # But you can patch that 
(https://github.com/psf/requests/issues/3341):
             self.http_client.request = functools.partial(
                 self.http_client.request, timeout=timeout)
+
+            # Enable a minimal retry. Better than nothing.
+            # 
https://github.com/psf/requests/blob/v2.25.1/requests/adapters.py#L94-L108
+            a = requests.adapters.HTTPAdapter(max_retries=1)
+            self.http_client.mount("http://";, a)
+            self.http_client.mount("https://";, a)
+
         self.app_name = app_name
         self.app_version = app_version
-        self.authority = Authority(
+
+        # Here the self.authority will not be the same type as authority in 
input
+        try:
+            self.authority = Authority(
                 authority or "https://login.microsoftonline.com/common/";,
                 self.http_client, validate_authority=validate_authority)
-            # Here the self.authority is not the same type as authority in 
input
+        except ValueError:  # Those are explicit authority validation errors
+            raise
+        except Exception:  # The rest are typically connection errors
+            if validate_authority and azure_region:
+                # Since caller opts in to use region, here we tolerate 
connection
+                # errors happened during authority validation at non-region 
endpoint
+                self.authority = Authority(
+                    authority or "https://login.microsoftonline.com/common/";,
+                    self.http_client, validate_authority=False)
+            else:
+                raise
+
         self.token_cache = token_cache or TokenCache()
-        self.client = self._build_client(client_credential, self.authority)
+        self._region_configured = azure_region
+        self._region_detected = None
+        self.client, self._regional_client = self._build_client(
+            client_credential, self.authority)
         self.authority_groups = None
+        self._telemetry_buffer = {}
+        self._telemetry_lock = Lock()
+
+    def _decorate_scope(
+            self, scopes,
+            reserved_scope=frozenset(['openid', 'profile', 'offline_access'])):
+        if not isinstance(scopes, (list, set, tuple)):
+            raise ValueError("The input scopes should be a list, tuple, or 
set")
+        scope_set = set(scopes)  # Input scopes is typically a list. Copy it 
to a set.
+        if scope_set & reserved_scope:
+            # These scopes are reserved for the API to provide good experience.
+            # We could make the developer pass these and then if they do they 
will
+            # come back asking why they don't see refresh token or user 
information.
+            raise ValueError(
+                "API does not accept {} value as user-provided scopes".format(
+                    reserved_scope))
+        if self.client_id in scope_set:
+            if len(scope_set) > 1:
+                # We make developers pass their client id, so that they can 
express
+                # the intent that they want the token for themselves (their own
+                # app).
+                # If we do not restrict them to passing only client id then 
they
+                # could write code where they expect an id token but end up 
getting
+                # access_token.
+                raise ValueError("Client Id can only be provided as a single 
scope")
+            decorated = set(reserved_scope)  # Make a writable copy
+        else:
+            decorated = scope_set | reserved_scope
+        decorated -= self._exclude_scopes
+        return list(decorated)
+
+    def _build_telemetry_context(
+            self, api_id, correlation_id=None, refresh_reason=None):
+        return msal.telemetry._TelemetryContext(
+            self._telemetry_buffer, self._telemetry_lock, api_id,
+            correlation_id=correlation_id, refresh_reason=refresh_reason)
+
+    def _get_regional_authority(self, central_authority):
+        is_region_specified = bool(self._region_configured
+            and self._region_configured != self.ATTEMPT_REGION_DISCOVERY)
+        self._region_detected = self._region_detected or _detect_region(
+            self.http_client if self._region_configured is not None else None)
+        if (is_region_specified and self._region_configured != 
self._region_detected):
+            logger.warning('Region configured ({}) != region detected 
({})'.format(
+                repr(self._region_configured), repr(self._region_detected)))
+        region_to_use = (
+            self._region_configured if is_region_specified else 
self._region_detected)
+        if region_to_use:
+            logger.info('Region to be used: {}'.format(repr(region_to_use)))
+            regional_host = ("{}.login.microsoft.com".format(region_to_use)
+                if central_authority.instance in (
+                    # The list came from 
https://github.com/AzureAD/microsoft-authentication-library-for-python/pull/358/files#r629400328
+                    "login.microsoftonline.com",
+                    "login.windows.net",
+                    "sts.windows.net",
+                    )
+                else "{}.{}".format(region_to_use, central_authority.instance))
+            return Authority(
+                "https://{}/{}".format(regional_host, 
central_authority.tenant),
+                self.http_client,
+                validate_authority=False)  # The central_authority has already 
been validated
+        return None
 
     def _build_client(self, client_credential, authority):
         client_assertion = None
@@ -289,15 +415,15 @@
             client_assertion_type = Client.CLIENT_ASSERTION_TYPE_JWT
         else:
             default_body['client_secret'] = client_credential
-        server_configuration = {
+        central_configuration = {
             "authorization_endpoint": authority.authorization_endpoint,
             "token_endpoint": authority.token_endpoint,
             "device_authorization_endpoint":
                 authority.device_authorization_endpoint or
                 urljoin(authority.token_endpoint, "devicecode"),
             }
-        return Client(
-            server_configuration,
+        central_client = Client(
+            central_configuration,
             self.client_id,
             http_client=self.http_client,
             default_headers=default_headers,
@@ -309,6 +435,31 @@
             on_removing_rt=self.token_cache.remove_rt,
             on_updating_rt=self.token_cache.update_rt)
 
+        regional_client = None
+        if client_credential:  # Currently regional endpoint only serves some 
CCA flows
+            regional_authority = self._get_regional_authority(authority)
+            if regional_authority:
+                regional_configuration = {
+                    "authorization_endpoint": 
regional_authority.authorization_endpoint,
+                    "token_endpoint": regional_authority.token_endpoint,
+                    "device_authorization_endpoint":
+                        regional_authority.device_authorization_endpoint or
+                        urljoin(regional_authority.token_endpoint, 
"devicecode"),
+                    }
+                regional_client = Client(
+                    regional_configuration,
+                    self.client_id,
+                    http_client=self.http_client,
+                    default_headers=default_headers,
+                    default_body=default_body,
+                    client_assertion=client_assertion,
+                    client_assertion_type=client_assertion_type,
+                    on_obtaining_tokens=lambda event: 
self.token_cache.add(dict(
+                        event, environment=authority.instance)),
+                    on_removing_rt=self.token_cache.remove_rt,
+                    on_updating_rt=self.token_cache.update_rt)
+        return central_client, regional_client
+
     def initiate_auth_code_flow(
             self,
             scopes,  # type: list[str]
@@ -325,7 +476,7 @@
         you can use :func:`~acquire_token_by_auth_code_flow()`
         to complete the authentication/authorization.
 
-        :param list scope:
+        :param list scopes:
             It is a list of case-sensitive strings.
         :param str redirect_uri:
             Optional. If not specified, server will use the pre-registered one.
@@ -373,7 +524,7 @@
         flow = client.initiate_auth_code_flow(
             redirect_uri=redirect_uri, state=state, login_hint=login_hint,
             prompt=prompt,
-            scope=decorate_scope(scopes, self.client_id),
+            scope=self._decorate_scope(scopes),
             domain_hint=domain_hint,
             claims=_merge_claims_challenge_and_capabilities(
                 self._client_capabilities, claims_challenge),
@@ -455,7 +606,7 @@
                 response_type=response_type,
                 redirect_uri=redirect_uri, state=state, login_hint=login_hint,
                 prompt=prompt,
-                scope=decorate_scope(scopes, self.client_id),
+                scope=self._decorate_scope(scopes),
                 nonce=nonce,
                 domain_hint=domain_hint,
                 claims=_merge_claims_challenge_and_capabilities(
@@ -513,21 +664,21 @@
                     return redirect(url_for("index"))
         """
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
-        return _clean_up(self.client.obtain_token_by_auth_code_flow(
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID)
+        response =_clean_up(self.client.obtain_token_by_auth_code_flow(
             auth_code_flow,
             auth_response,
-            scope=decorate_scope(scopes, self.client_id) if scopes else None,
-            headers={
-                CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID),
-                },
+            scope=self._decorate_scope(scopes) if scopes else None,
+            headers=telemetry_context.generate_headers(),
             data=dict(
                 kwargs.pop("data", {}),
                 claims=_merge_claims_challenge_and_capabilities(
                     self._client_capabilities,
                     auth_code_flow.pop("claims_challenge", None))),
             **kwargs))
+        telemetry_context.update_telemetry(response)
+        return response
 
     def acquire_token_by_authorization_code(
             self,
@@ -586,20 +737,20 @@
             "Change your acquire_token_by_authorization_code() "
             "to acquire_token_by_auth_code_flow()", DeprecationWarning)
         with warnings.catch_warnings(record=True):
-            return _clean_up(self.client.obtain_token_by_authorization_code(
+            telemetry_context = self._build_telemetry_context(
+                self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID)
+            response = 
_clean_up(self.client.obtain_token_by_authorization_code(
                 code, redirect_uri=redirect_uri,
-                scope=decorate_scope(scopes, self.client_id),
-                headers={
-                    CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                    CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                        self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID),
-                    },
+                scope=self._decorate_scope(scopes),
+                headers=telemetry_context.generate_headers(),
                 data=dict(
                     kwargs.pop("data", {}),
                     claims=_merge_claims_challenge_and_capabilities(
                         self._client_capabilities, claims_challenge)),
                 nonce=nonce,
                 **kwargs))
+            telemetry_context.update_telemetry(response)
+            return response
 
     def get_accounts(self, username=None):
         """Get a list of accounts which previously signed in, i.e. exists in 
cache.
@@ -625,6 +776,13 @@
             lowercase_username = username.lower()
             accounts = [a for a in accounts
                 if a["username"].lower() == lowercase_username]
+            if not accounts:
+                logger.warning((
+                    "get_accounts(username='{}') finds no account. "
+                    "If tokens were acquired without 'profile' scope, "
+                    "they would contain no username for filtering. "
+                    "Consider calling get_accounts(username=None) instead."
+                    ).format(username))
         # Does not further filter by existing RTs here. It probably won't 
matter.
         # Because in most cases Accounts and RTs co-exist.
         # Even in the rare case when an RT is revoked and then removed,
@@ -633,10 +791,25 @@
         return accounts
 
     def _find_msal_accounts(self, environment):
-        return [a for a in self.token_cache.find(
-            TokenCache.CredentialType.ACCOUNT, query={"environment": 
environment})
+        grouped_accounts = {
+            a.get("home_account_id"):  # Grouped by home tenant's id
+                {  # These are minimal amount of non-tenant-specific account 
info
+                    "home_account_id": a.get("home_account_id"),
+                    "environment": a.get("environment"),
+                    "username": a.get("username"),
+
+                    # The following fields for backward compatibility, for now
+                    "authority_type": a.get("authority_type"),
+                    "local_account_id": a.get("local_account_id"),  # 
Tenant-specific
+                    "realm": a.get("realm"),  # Tenant-specific
+                }
+            for a in self.token_cache.find(
+                TokenCache.CredentialType.ACCOUNT,
+                query={"environment": environment})
             if a["authority_type"] in (
-                TokenCache.AuthorityType.ADFS, TokenCache.AuthorityType.MSSTS)]
+                TokenCache.AuthorityType.ADFS, TokenCache.AuthorityType.MSSTS)
+            }
+        return list(grouped_accounts.values())
 
     def _get_authority_aliases(self, instance):
         if not self.authority_groups:
@@ -728,7 +901,7 @@
             - None when cache lookup does not yield a token.
         """
         result = self.acquire_token_silent_with_error(
-            scopes, account, authority, force_refresh,
+            scopes, account, authority=authority, force_refresh=force_refresh,
             claims_challenge=claims_challenge, **kwargs)
         return result if result and "error" not in result else None
 
@@ -773,7 +946,7 @@
         """
         assert isinstance(scopes, list), "Invalid parameter type"
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
-        correlation_id = _get_new_correlation_id()
+        correlation_id = msal.telemetry._get_new_correlation_id()
         if authority:
             warnings.warn("We haven't decided how/if this method will accept 
authority parameter")
         # the_authority = Authority(
@@ -844,9 +1017,11 @@
                 target=scopes,
                 query=query)
             now = time.time()
+            refresh_reason = msal.telemetry.AT_ABSENT
             for entry in matches:
                 expires_in = int(entry["expires_on"]) - now
                 if expires_in < 5*60:  # Then consider it expired
+                    refresh_reason = msal.telemetry.AT_EXPIRED
                     continue  # Removal is not necessary, it will be 
overwritten
                 logger.debug("Cache hit an AT")
                 access_token_from_cache = {  # Mimic a real response
@@ -855,13 +1030,18 @@
                     "expires_in": int(expires_in),  # OAuth2 specs defines it 
as int
                     }
                 if "refresh_on" in entry and int(entry["refresh_on"]) < now:  
# aging
+                    refresh_reason = msal.telemetry.AT_AGING
                     break  # With a fallback in hand, we break here to go 
refresh
+                self._build_telemetry_context(-1).hit_an_access_token()
                 return access_token_from_cache  # It is still good as new
+        else:
+            refresh_reason = msal.telemetry.FORCE_REFRESH  # TODO: It could 
also mean claims_challenge
+        assert refresh_reason, "It should have been established at this point"
         try:
-            result = 
self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
-                authority, decorate_scope(scopes, self.client_id), account,
-                force_refresh=force_refresh, 
claims_challenge=claims_challenge, **kwargs)
-            result = _clean_up(result)
+            result = 
_clean_up(self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
+                authority, self._decorate_scope(scopes), account,
+                refresh_reason=refresh_reason, 
claims_challenge=claims_challenge,
+                **kwargs))
             if (result and "error" not in result) or (not 
access_token_from_cache):
                 return result
         except:  # The exact HTTP exception is transportation-layer dependent
@@ -915,15 +1095,19 @@
     def _acquire_token_silent_by_finding_specific_refresh_token(
             self, authority, scopes, query,
             rt_remover=None, break_condition=lambda response: False,
-            force_refresh=False, correlation_id=None, claims_challenge=None, 
**kwargs):
+            refresh_reason=None, correlation_id=None, claims_challenge=None,
+            **kwargs):
         matches = self.token_cache.find(
             self.token_cache.CredentialType.REFRESH_TOKEN,
             # target=scopes,  # AAD RTs are scope-independent
             query=query)
         logger.debug("Found %d RTs matching %s", len(matches), query)
-        client = self._build_client(self.client_credential, authority)
+        client, _ = self._build_client(self.client_credential, authority)
 
         response = None  # A distinguishable value to mean cache is empty
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_SILENT_ID,
+            correlation_id=correlation_id, refresh_reason=refresh_reason)
         for entry in sorted(  # Since unfit RTs would not be aggressively 
removed,
                               # we start from newer RTs which are more likely 
fit.
                 matches,
@@ -941,16 +1125,13 @@
                     skip_account_creation=True,  # To honor a concurrent 
remove_account()
                     )),
                 scope=scopes,
-                headers={
-                    CLIENT_REQUEST_ID: correlation_id or 
_get_new_correlation_id(),
-                    CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                        self.ACQUIRE_TOKEN_SILENT_ID, 
force_refresh=force_refresh),
-                    },
+                headers=telemetry_context.generate_headers(),
                 data=dict(
                     kwargs.pop("data", {}),
                     claims=_merge_claims_challenge_and_capabilities(
                         self._client_capabilities, claims_challenge)),
                 **kwargs)
+            telemetry_context.update_telemetry(response)
             if "error" not in response:
                 return response
             logger.debug("Refresh failed. {error}: {error_description}".format(
@@ -999,18 +1180,110 @@
             * A dict contains no "error" key means migration was successful.
         """
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
-        return _clean_up(self.client.obtain_token_by_refresh_token(
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_BY_REFRESH_TOKEN,
+            refresh_reason=msal.telemetry.FORCE_REFRESH)
+        response = _clean_up(self.client.obtain_token_by_refresh_token(
             refresh_token,
-            scope=decorate_scope(scopes, self.client_id),
-            headers={
-                CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_BY_REFRESH_TOKEN),
-            },
+            scope=self._decorate_scope(scopes),
+            headers=telemetry_context.generate_headers(),
             rt_getter=lambda rt: rt,
             on_updating_rt=False,
             on_removing_rt=lambda rt_item: None,  # No OP
             **kwargs))
+        telemetry_context.update_telemetry(response)
+        return response
+
+    def acquire_token_by_username_password(
+            self, username, password, scopes, claims_challenge=None, **kwargs):
+        """Gets a token for a given resource via user credentials.
+
+        See this page for constraints of Username Password Flow.
+        
https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication
+
+        :param str username: Typically a UPN in the form of an email address.
+        :param str password: The password.
+        :param list[str] scopes:
+            Scopes requested to access a protected API (a resource).
+        :param claims_challenge:
+            The claims_challenge parameter requests specific claims requested 
by the resource provider
+            in the form of a claims_challenge directive in the 
www-authenticate header to be
+            returned from the UserInfo Endpoint and/or in the ID Token and/or 
Access Token.
+            It is a string of a JSON object which contains lists of claims 
being requested from these locations.
+
+        :return: A dict representing the json response from AAD:
+
+            - A successful response would contain "access_token" key,
+            - an error response would contain "error" and usually 
"error_description".
+        """
+        scopes = self._decorate_scope(scopes)
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID)
+        headers = telemetry_context.generate_headers()
+        data = dict(
+            kwargs.pop("data", {}),
+            claims=_merge_claims_challenge_and_capabilities(
+                self._client_capabilities, claims_challenge))
+        if not self.authority.is_adfs:
+            user_realm_result = self.authority.user_realm_discovery(
+                username, 
correlation_id=headers[msal.telemetry.CLIENT_REQUEST_ID])
+            if user_realm_result.get("account_type") == "Federated":
+                response = 
_clean_up(self._acquire_token_by_username_password_federated(
+                    user_realm_result, username, password, scopes=scopes,
+                    data=data,
+                    headers=headers, **kwargs))
+                telemetry_context.update_telemetry(response)
+                return response
+        response = _clean_up(self.client.obtain_token_by_username_password(
+                username, password, scope=scopes,
+                headers=headers,
+                data=data,
+                **kwargs))
+        telemetry_context.update_telemetry(response)
+        return response
+
+    def _acquire_token_by_username_password_federated(
+            self, user_realm_result, username, password, scopes=None, 
**kwargs):
+        wstrust_endpoint = {}
+        if user_realm_result.get("federation_metadata_url"):
+            wstrust_endpoint = mex_send_request(
+                user_realm_result["federation_metadata_url"],
+                self.http_client)
+            if wstrust_endpoint is None:
+                raise ValueError("Unable to find wstrust endpoint from MEX. "
+                    "This typically happens when attempting MSA accounts. "
+                    "More details available here. "
+                    
"https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication";)
+        logger.debug("wstrust_endpoint = %s", wstrust_endpoint)
+        wstrust_result = wst_send_request(
+            username, password,
+            user_realm_result.get("cloud_audience_urn", 
"urn:federation:MicrosoftOnline"),
+            wstrust_endpoint.get("address",
+                # Fallback to an AAD supplied endpoint
+                user_realm_result.get("federation_active_auth_url")),
+            wstrust_endpoint.get("action"), self.http_client)
+        if not ("token" in wstrust_result and "type" in wstrust_result):
+            raise RuntimeError("Unsuccessful RSTR. %s" % wstrust_result)
+        GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
+        grant_type = {
+            SAML_TOKEN_TYPE_V1: GRANT_TYPE_SAML1_1,
+            SAML_TOKEN_TYPE_V2: self.client.GRANT_TYPE_SAML2,
+            WSS_SAML_TOKEN_PROFILE_V1_1: GRANT_TYPE_SAML1_1,
+            WSS_SAML_TOKEN_PROFILE_V2: self.client.GRANT_TYPE_SAML2
+            }.get(wstrust_result.get("type"))
+        if not grant_type:
+            raise RuntimeError(
+                "RSTR returned unknown token type: %s", 
wstrust_result.get("type"))
+        self.client.grant_assertion_encoders.setdefault(  # Register a 
non-standard type
+            grant_type, self.client.encode_saml_assertion)
+        return self.client.obtain_token_by_assertion(
+            wstrust_result["token"], grant_type, scope=scopes,
+            on_obtaining_tokens=lambda event: self.token_cache.add(dict(
+                event,
+                environment=self.authority.instance,
+                username=username,  # Useful in case IDT contains no such info
+                )),
+            **kwargs)
 
 
 class PublicClientApplication(ClientApplication):  # browser app or mobile app
@@ -1039,7 +1312,7 @@
         Prerequisite: In Azure Portal, configure the Redirect URI of your
         "Mobile and Desktop application" as ``http://localhost``.
 
-        :param list scope:
+        :param list scopes:
             It is a list of case-sensitive strings.
         :param str prompt:
             By default, no prompt value will be sent, not even "none".
@@ -1080,15 +1353,16 @@
 
         :return:
             - A dict containing no "error" key,
-              and typically contains an "access_token" key,
-              if cache lookup succeeded.
+              and typically contains an "access_token" key.
             - A dict containing an "error" key, when token refresh failed.
         """
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
         claims = _merge_claims_challenge_and_capabilities(
             self._client_capabilities, claims_challenge)
-        return _clean_up(self.client.obtain_token_by_browser(
-            scope=decorate_scope(scopes, self.client_id) if scopes else None,
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_INTERACTIVE)
+        response = _clean_up(self.client.obtain_token_by_browser(
+            scope=self._decorate_scope(scopes) if scopes else None,
             extra_scope_to_consent=extra_scopes_to_consent,
             redirect_uri="http://localhost:{port}".format(
                 # Hardcode the host, for now. AAD portal rejects 127.0.0.1 
anyway
@@ -1101,12 +1375,10 @@
                 "domain_hint": domain_hint,
                 },
             data=dict(kwargs.pop("data", {}), claims=claims),
-            headers={
-                CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_INTERACTIVE),
-                },
+            headers=telemetry_context.generate_headers(),
             **kwargs))
+        telemetry_context.update_telemetry(response)
+        return response
 
     def initiate_device_flow(self, scopes=None, **kwargs):
         """Initiate a Device Flow instance,
@@ -1119,13 +1391,10 @@
             - A successful response would contain "user_code" key, among others
             - an error response would contain some other readable key/value 
pairs.
         """
-        correlation_id = _get_new_correlation_id()
+        correlation_id = msal.telemetry._get_new_correlation_id()
         flow = self.client.initiate_device_flow(
-            scope=decorate_scope(scopes or [], self.client_id),
-            headers={
-                CLIENT_REQUEST_ID: correlation_id,
-                # CLIENT_CURRENT_TELEMETRY is not currently required
-                },
+            scope=self._decorate_scope(scopes or []),
+            headers={msal.telemetry.CLIENT_REQUEST_ID: correlation_id},
             **kwargs)
         flow[self.DEVICE_FLOW_CORRELATION_ID] = correlation_id
         return flow
@@ -1149,7 +1418,10 @@
             - A successful response would contain "access_token" key,
             - an error response would contain "error" and usually 
"error_description".
         """
-        return _clean_up(self.client.obtain_token_by_device_flow(
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID,
+            correlation_id=flow.get(self.DEVICE_FLOW_CORRELATION_ID))
+        response = _clean_up(self.client.obtain_token_by_device_flow(
             flow,
             data=dict(
                 kwargs.pop("data", {}),
@@ -1159,96 +1431,10 @@
                 claims=_merge_claims_challenge_and_capabilities(
                     self._client_capabilities, claims_challenge),
                 ),
-            headers={
-                CLIENT_REQUEST_ID:
-                    flow.get(self.DEVICE_FLOW_CORRELATION_ID) or 
_get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID),
-                },
+            headers=telemetry_context.generate_headers(),
             **kwargs))
-
-    def acquire_token_by_username_password(
-            self, username, password, scopes, claims_challenge=None, **kwargs):
-        """Gets a token for a given resource via user credentials.
-
-        See this page for constraints of Username Password Flow.
-        
https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication
-
-        :param str username: Typically a UPN in the form of an email address.
-        :param str password: The password.
-        :param list[str] scopes:
-            Scopes requested to access a protected API (a resource).
-        :param claims_challenge:
-            The claims_challenge parameter requests specific claims requested 
by the resource provider
-            in the form of a claims_challenge directive in the 
www-authenticate header to be
-            returned from the UserInfo Endpoint and/or in the ID Token and/or 
Access Token.
-            It is a string of a JSON object which contains lists of claims 
being requested from these locations.
-
-        :return: A dict representing the json response from AAD:
-
-            - A successful response would contain "access_token" key,
-            - an error response would contain "error" and usually 
"error_description".
-        """
-        scopes = decorate_scope(scopes, self.client_id)
-        headers = {
-            CLIENT_REQUEST_ID: _get_new_correlation_id(),
-            CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header(
-                self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID),
-            }
-        data = dict(
-            kwargs.pop("data", {}),
-            claims=_merge_claims_challenge_and_capabilities(
-                self._client_capabilities, claims_challenge))
-        if not self.authority.is_adfs:
-            user_realm_result = self.authority.user_realm_discovery(
-                username, correlation_id=headers[CLIENT_REQUEST_ID])
-            if user_realm_result.get("account_type") == "Federated":
-                return 
_clean_up(self._acquire_token_by_username_password_federated(
-                    user_realm_result, username, password, scopes=scopes,
-                    data=data,
-                    headers=headers, **kwargs))
-        return _clean_up(self.client.obtain_token_by_username_password(
-                username, password, scope=scopes,
-                headers=headers,
-                data=data,
-                **kwargs))
-
-    def _acquire_token_by_username_password_federated(
-            self, user_realm_result, username, password, scopes=None, 
**kwargs):
-        wstrust_endpoint = {}
-        if user_realm_result.get("federation_metadata_url"):
-            wstrust_endpoint = mex_send_request(
-                user_realm_result["federation_metadata_url"],
-                self.http_client)
-            if wstrust_endpoint is None:
-                raise ValueError("Unable to find wstrust endpoint from MEX. "
-                    "This typically happens when attempting MSA accounts. "
-                    "More details available here. "
-                    
"https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication";)
-        logger.debug("wstrust_endpoint = %s", wstrust_endpoint)
-        wstrust_result = wst_send_request(
-            username, password,
-            user_realm_result.get("cloud_audience_urn", 
"urn:federation:MicrosoftOnline"),
-            wstrust_endpoint.get("address",
-                # Fallback to an AAD supplied endpoint
-                user_realm_result.get("federation_active_auth_url")),
-            wstrust_endpoint.get("action"), self.http_client)
-        if not ("token" in wstrust_result and "type" in wstrust_result):
-            raise RuntimeError("Unsuccessful RSTR. %s" % wstrust_result)
-        GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
-        grant_type = {
-            SAML_TOKEN_TYPE_V1: GRANT_TYPE_SAML1_1,
-            SAML_TOKEN_TYPE_V2: self.client.GRANT_TYPE_SAML2,
-            WSS_SAML_TOKEN_PROFILE_V1_1: GRANT_TYPE_SAML1_1,
-            WSS_SAML_TOKEN_PROFILE_V2: self.client.GRANT_TYPE_SAML2
-            }.get(wstrust_result.get("type"))
-        if not grant_type:
-            raise RuntimeError(
-                "RSTR returned unknown token type: %s", 
wstrust_result.get("type"))
-        self.client.grant_assertion_encoders.setdefault(  # Register a 
non-standard type
-            grant_type, self.client.encode_saml_assertion)
-        return self.client.obtain_token_by_assertion(
-            wstrust_result["token"], grant_type, scope=scopes, **kwargs)
+        telemetry_context.update_telemetry(response)
+        return response
 
 
 class ConfidentialClientApplication(ClientApplication):  # server-side web app
@@ -1271,18 +1457,19 @@
         """
         # TBD: force_refresh behavior
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
-        return _clean_up(self.client.obtain_token_for_client(
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_FOR_CLIENT_ID)
+        client = self._regional_client or self.client
+        response = _clean_up(client.obtain_token_for_client(
             scope=scopes,  # This grant flow requires no scope decoration
-            headers={
-                CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_FOR_CLIENT_ID),
-                },
+            headers=telemetry_context.generate_headers(),
             data=dict(
                 kwargs.pop("data", {}),
                 claims=_merge_claims_challenge_and_capabilities(
                     self._client_capabilities, claims_challenge)),
             **kwargs))
+        telemetry_context.update_telemetry(response)
+        return response
 
     def acquire_token_on_behalf_of(self, user_assertion, scopes, 
claims_challenge=None, **kwargs):
         """Acquires token using on-behalf-of (OBO) flow.
@@ -1310,12 +1497,14 @@
             - A successful response would contain "access_token" key,
             - an error response would contain "error" and usually 
"error_description".
         """
+        telemetry_context = self._build_telemetry_context(
+            self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID)
         # The implementation is NOT based on Token Exchange
         # https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
-        return _clean_up(self.client.obtain_token_by_assertion(  # bases on 
assertion RFC 7521
+        response = _clean_up(self.client.obtain_token_by_assertion(  # bases 
on assertion RFC 7521
             user_assertion,
             self.client.GRANT_TYPE_JWT,  # IDTs and AAD ATs are all JWTs
-            scope=decorate_scope(scopes, self.client_id),  # Decoration is 
used for:
+            scope=self._decorate_scope(scopes),  # Decoration is used for:
                 # 1. Explicitly requesting an RT, without relying on AAD 
default
                 #    behavior, even though it currently still issues an RT.
                 # 2. Requesting an IDT (which would otherwise be unavailable)
@@ -1326,9 +1515,7 @@
                 requested_token_use="on_behalf_of",
                 claims=_merge_claims_challenge_and_capabilities(
                     self._client_capabilities, claims_challenge)),
-            headers={
-                CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID),
-                },
+            headers=telemetry_context.generate_headers(),
             **kwargs))
+        telemetry_context.update_telemetry(response)
+        return response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/oauth2cli/authcode.py 
new/msal-1.12.0/msal/oauth2cli/authcode.py
--- old/msal-1.10.0/msal/oauth2cli/authcode.py  2021-03-08 21:46:19.000000000 
+0100
+++ new/msal-1.12.0/msal/oauth2cli/authcode.py  2021-05-19 22:28:05.000000000 
+0200
@@ -33,9 +33,34 @@
             ).get("code")
 
 
+def is_wsl():
+    # "Official" way of detecting WSL: 
https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
+    # Run `uname -a` to get 'release' without python
+    #   - WSL 1: '4.4.0-19041-Microsoft'
+    #   - WSL 2: '4.19.128-microsoft-standard'
+    import platform
+    uname = platform.uname()
+    platform_name = getattr(uname, 'system', uname[0]).lower()
+    release = getattr(uname, 'release', uname[2]).lower()
+    return platform_name == 'linux' and 'microsoft' in release
+
+
 def _browse(auth_uri):  # throws ImportError, possibly webbrowser.Error in 
future
     import webbrowser  # Lazy import. Some distro may not have this.
-    return webbrowser.open(auth_uri)  # Use default browser. Customizable by 
$BROWSER
+    browser_opened = webbrowser.open(auth_uri)  # Use default browser. 
Customizable by $BROWSER
+
+    # In WSL which doesn't have www-browser, try launching browser with 
PowerShell
+    if not browser_opened and is_wsl():
+        try:
+            import subprocess
+            # 
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe
+            # Ampersand (&) should be quoted
+            exit_code = subprocess.call(
+                ['powershell.exe', '-NoProfile', '-Command', 'Start-Process 
"{}"'.format(auth_uri)])
+            browser_opened = exit_code == 0
+        except FileNotFoundError:  # WSL might be too old
+            pass
+    return browser_opened
 
 
 def _qs2kv(qs):
@@ -245,4 +270,3 @@
             timeout=60,
             state=flow["state"],  # Optional
             ), indent=4))
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/oauth2cli/oauth2.py 
new/msal-1.12.0/msal/oauth2cli/oauth2.py
--- old/msal-1.10.0/msal/oauth2cli/oauth2.py    2021-03-08 21:46:19.000000000 
+0100
+++ new/msal-1.12.0/msal/oauth2cli/oauth2.py    2021-05-19 22:28:05.000000000 
+0200
@@ -770,7 +770,6 @@
             rt_getter=lambda token_item: token_item["refresh_token"],
             on_removing_rt=None,
             on_updating_rt=None,
-            on_obtaining_tokens=None,
             **kwargs):
         # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> 
dict
         """This is an overload which will trigger token storage callbacks.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/oauth2cli/oidc.py 
new/msal-1.12.0/msal/oauth2cli/oidc.py
--- old/msal-1.10.0/msal/oauth2cli/oidc.py      2021-03-08 21:46:19.000000000 
+0100
+++ new/msal-1.12.0/msal/oauth2cli/oidc.py      2021-05-19 22:28:05.000000000 
+0200
@@ -83,6 +83,19 @@
     return hashlib.sha256(nonce.encode("ascii")).hexdigest()
 
 
+class Prompt(object):
+    """This class defines the constant strings for prompt parameter.
+
+    The values are based on
+    https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+    """
+    NONE = "none"
+    LOGIN = "login"
+    CONSENT = "consent"
+    SELECT_ACCOUNT = "select_account"
+    CREATE = "create"  # Defined in 
https://openid.net/specs/openid-connect-prompt-create-1_0.html#PromptParameter
+
+
 class Client(oauth2.Client):
     """OpenID Connect is a layer on top of the OAuth2.
 
@@ -217,6 +230,8 @@
             `OIDC 
<https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_.
         :param string prompt: Defined in
             `OIDC 
<https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_.
+            You can find the valid string values defined in 
:class:`oidc.Prompt`.
+
         :param int max_age: Defined in
             `OIDC 
<https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_.
         :param string ui_locales: Defined in
@@ -232,7 +247,7 @@
         for descriptions on other parameters and return value.
         """
         filtered_params = {k:v for k, v in dict(
-            prompt=prompt,
+            prompt=" ".join(prompt) if isinstance(prompt, (list, tuple)) else 
prompt,
             display=display,
             max_age=max_age,
             ui_locales=ui_locales,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/region.py 
new/msal-1.12.0/msal/region.py
--- old/msal-1.10.0/msal/region.py      1970-01-01 01:00:00.000000000 +0100
+++ new/msal-1.12.0/msal/region.py      2021-05-19 22:28:05.000000000 +0200
@@ -0,0 +1,47 @@
+import os
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def _detect_region(http_client=None):
+    region = _detect_region_of_azure_function()  # It is cheap, so we do it 
always
+    if http_client and not region:
+        return _detect_region_of_azure_vm(http_client)  # It could hang for 
minutes
+    return region
+
+
+def _detect_region_of_azure_function():
+    return os.environ.get("REGION_NAME")
+
+
+def _detect_region_of_azure_vm(http_client):
+    url = (
+        "http://169.254.169.254/metadata/instance";
+
+        # Utilize the "route parameters" feature to obtain region as a string
+        # 
https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#route-parameters
+        "/compute/location?format=text"
+
+        # Location info is available since API version 2017-04-02
+        # 
https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#response-1
+        "&api-version=2021-01-01"
+        )
+    logger.info(
+        "Connecting to IMDS {}. "
+        "It may take a while if you are running outside of Azure. "
+        "You should consider opting in/out region behavior on-demand, "
+        'by loading a boolean flag "is_deployed_in_azure" '
+        'from your per-deployment config and then do '
+        '"app = ConfidentialClientApplication(..., '
+        'azure_region=is_deployed_in_azure)"'.format(url))
+    try:
+        # 
https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#instance-metadata
+        resp = http_client.get(url, headers={"Metadata": "true"})
+    except:
+        logger.info(
+            "IMDS {} unavailable. Perhaps not running in Azure 
VM?".format(url))
+        return None
+    else:
+        return resp.text.strip()
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/telemetry.py 
new/msal-1.12.0/msal/telemetry.py
--- old/msal-1.10.0/msal/telemetry.py   1970-01-01 01:00:00.000000000 +0100
+++ new/msal-1.12.0/msal/telemetry.py   2021-05-19 22:28:05.000000000 +0200
@@ -0,0 +1,78 @@
+import uuid
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+CLIENT_REQUEST_ID = 'client-request-id'
+CLIENT_CURRENT_TELEMETRY = "x-client-current-telemetry"
+CLIENT_LAST_TELEMETRY = "x-client-last-telemetry"
+NON_SILENT_CALL = 0
+FORCE_REFRESH = 1
+AT_ABSENT = 2
+AT_EXPIRED = 3
+AT_AGING = 4
+RESERVED = 5
+
+
+def _get_new_correlation_id():
+    return str(uuid.uuid4())
+
+
+class _TelemetryContext(object):
+    """It is used for handling the telemetry context for current OAuth2 
"exchange"."""
+    # 
https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview?path=%2FTelemetry%2FMSALServerSideTelemetry.md&_a=preview
+    _SUCCEEDED = "succeeded"
+    _FAILED = "failed"
+    _FAILURE_SIZE = "failure_size"
+    _CURRENT_HEADER_SIZE_LIMIT = 100
+    _LAST_HEADER_SIZE_LIMIT = 350
+
+    def __init__(self, buffer, lock, api_id, correlation_id=None, 
refresh_reason=None):
+        self._buffer = buffer
+        self._lock = lock
+        self._api_id = api_id
+        self._correlation_id = correlation_id or _get_new_correlation_id()
+        self._refresh_reason = refresh_reason or NON_SILENT_CALL
+        logger.debug("Generate or reuse correlation_id: %s", 
self._correlation_id)
+
+    def generate_headers(self):
+        with self._lock:
+            current = "4|{api_id},{cache_refresh}|".format(
+                api_id=self._api_id, cache_refresh=self._refresh_reason)
+            if len(current) > self._CURRENT_HEADER_SIZE_LIMIT:
+                logger.warning(
+                    "Telemetry header greater than {} will be truncated by 
AAD".format(
+                    self._CURRENT_HEADER_SIZE_LIMIT))
+            failures = self._buffer.get(self._FAILED, [])
+            return {
+                CLIENT_REQUEST_ID: self._correlation_id,
+                CLIENT_CURRENT_TELEMETRY: current,
+                CLIENT_LAST_TELEMETRY: 
"4|{succeeded}|{failed_requests}|{errors}|".format(
+                    succeeded=self._buffer.get(self._SUCCEEDED, 0),
+                    failed_requests=",".join("{a},{c}".format(**f) for f in 
failures),
+                    errors=",".join(f["e"] for f in failures),
+                    )
+                }
+
+    def hit_an_access_token(self):
+        with self._lock:
+            self._buffer[self._SUCCEEDED] = self._buffer.get(self._SUCCEEDED, 
0) + 1
+
+    def update_telemetry(self, auth_result):
+        if auth_result:
+            with self._lock:
+                if "error" in auth_result:
+                    self._record_failure(auth_result["error"])
+                else:  # Telemetry sent successfully. Reset buffer
+                    self._buffer.clear()  # This won't work: self._buffer = {}
+
+    def _record_failure(self, error):
+        simulation = len(",{api_id},{correlation_id},{error}".format(
+            api_id=self._api_id, correlation_id=self._correlation_id, 
error=error))
+        if self._buffer.get(self._FAILURE_SIZE, 0) + simulation < 
self._LAST_HEADER_SIZE_LIMIT:
+            self._buffer[self._FAILURE_SIZE] = self._buffer.get(
+                self._FAILURE_SIZE, 0) + simulation
+            self._buffer.setdefault(self._FAILED, []).append({
+                "a": self._api_id, "c": self._correlation_id, "e": error})
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/token_cache.py 
new/msal-1.12.0/msal/token_cache.py
--- old/msal-1.10.0/msal/token_cache.py 2021-03-08 21:46:19.000000000 +0100
+++ new/msal-1.12.0/msal/token_cache.py 2021-05-19 22:28:05.000000000 +0200
@@ -108,11 +108,13 @@
                 if sensitive in dictionary:
                     dictionary[sensitive] = "********"
         wipe(event.get("data", {}),
-            ("password", "client_secret", "refresh_token", "assertion", 
"username"))
+            ("password", "client_secret", "refresh_token", "assertion"))
         try:
             return self.__add(event, now=now)
         finally:
-            wipe(event.get("response", {}), ("access_token", "refresh_token"))
+            wipe(event.get("response", {}), (  # These claims were useful 
during __add()
+                "access_token", "refresh_token", "id_token", "username"))
+            wipe(event, ["username"])  # Needed for federated ROPC
             logger.debug("event=%s", json.dumps(
             # We examined and concluded that this log won't have Log Injection 
risk,
             # because the event payload is already in JSON so CR/LF will be 
escaped.
@@ -145,7 +147,7 @@
             client_info["uid"] = id_token_claims.get("sub")
             home_account_id = id_token_claims.get("sub")
 
-        target = ' '.join(event.get("scope", []))  # Per schema, we don't sort 
it
+        target = ' '.join(event.get("scope") or [])  # Per schema, we don't 
sort it
 
         with self._lock:
             now = int(time.time() if now is None else now)
@@ -184,6 +186,8 @@
                         "oid", id_token_claims.get("sub")),
                     "username": id_token_claims.get("preferred_username")  # 
AAD
                         or id_token_claims.get("upn")  # ADFS 2019
+                        or data.get("username")  # Falls back to ROPC username
+                        or event.get("username")  # Falls back to Federated 
ROPC username
                         or "",  # The schema does not like null
                     "authority_type":
                         self.AuthorityType.ADFS if realm == "adfs"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal/wstrust_response.py 
new/msal-1.12.0/msal/wstrust_response.py
--- old/msal-1.10.0/msal/wstrust_response.py    2021-03-08 21:46:19.000000000 
+0100
+++ new/msal-1.12.0/msal/wstrust_response.py    2021-05-19 22:28:05.000000000 
+0200
@@ -88,5 +88,7 @@
         token_types = findall_content(rstr, "TokenType")
         tokens = findall_content(rstr, "RequestedSecurityToken")
         if token_types and tokens:
-            return {"token": tokens[0].encode('us-ascii'), "type": 
token_types[0]}
+            # Historically, we use "us-ascii" encoding, but it should be 
"utf-8"
+            # 
https://stackoverflow.com/questions/36658000/what-is-encoding-used-for-saml-conversations
+            return {"token": tokens[0].encode('utf-8'), "type": token_types[0]}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal.egg-info/PKG-INFO 
new/msal-1.12.0/msal.egg-info/PKG-INFO
--- old/msal-1.10.0/msal.egg-info/PKG-INFO      2021-03-08 21:46:32.000000000 
+0100
+++ new/msal-1.12.0/msal.egg-info/PKG-INFO      2021-05-19 22:28:13.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: msal
-Version: 1.10.0
+Version: 1.12.0
 Summary: The Microsoft Authentication Library (MSAL) for Python library 
enables your app to access the Microsoft Cloud by supporting authentication of 
users with Microsoft Azure Active Directory accounts (AAD) and Microsoft 
Accounts (MSA) using industry standard OAuth2 and OpenID Connect.
 Home-page: 
https://github.com/AzureAD/microsoft-authentication-library-for-python
 Author: Microsoft Corporation
@@ -21,8 +21,14 @@
         
         Quick links:
         
-        | [Getting 
Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp)
 | 
[Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki)
 | [Samples](https://aka.ms/aaddevsamplesv2) | 
[Support](README.md#community-help-and-support)
-        | --- | --- | --- | --- |
+        | [Getting 
Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp)
 | 
[Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki)
 | [Samples](https://aka.ms/aaddevsamplesv2) | 
[Support](README.md#community-help-and-support) | 
[Feedback](https://forms.office.com/r/TMjZkDbzjY) |
+        | --- | --- | --- | --- | --- |
+        
+        ## Scenarios supported
+        
+        Click on the following thumbnail to visit a large map with clickable 
links to proper samples.
+        
+        [![Map effect won't work inside github's markdown file, so we have to 
use a thumbnail here to lure audience to a real static 
website](docs/thumbnail.png)](https://msal-python.readthedocs.io/en/latest/)
         
         ## Installation
         
@@ -135,6 +141,9 @@
         Here is the latest Q&A on Stack Overflow for MSAL:
         
[http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal)
         
+        ## Submit Feedback
+        We'd like your thoughts on this library. Please complete [this short 
survey.](https://forms.office.com/r/TMjZkDbzjY)
+        
         ## Security Reporting
         
         If you find a security issue with our libraries or services please 
report it to [sec...@microsoft.com](mailto:sec...@microsoft.com) with as much 
detail as possible. Your submission may be eligible for a bounty through the 
[Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post 
security issues to GitHub Issues or any other public site. We will contact you 
shortly upon receiving the information. We encourage you to get notifications 
of when security incidents occur by visiting [this 
page](https://technet.microsoft.com/security/dd252948) and subscribing to 
Security Advisory Alerts.
@@ -153,11 +162,11 @@
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: OS Independent
 Description-Content-Type: text/markdown
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal.egg-info/SOURCES.txt 
new/msal-1.12.0/msal.egg-info/SOURCES.txt
--- old/msal-1.10.0/msal.egg-info/SOURCES.txt   2021-03-08 21:46:32.000000000 
+0100
+++ new/msal-1.12.0/msal.egg-info/SOURCES.txt   2021-05-19 22:28:13.000000000 
+0200
@@ -1,3 +1,4 @@
+LICENSE
 README.md
 setup.cfg
 setup.py
@@ -6,6 +7,8 @@
 msal/authority.py
 msal/exceptions.py
 msal/mex.py
+msal/region.py
+msal/telemetry.py
 msal/token_cache.py
 msal/wstrust_request.py
 msal/wstrust_response.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/msal.egg-info/requires.txt 
new/msal-1.12.0/msal.egg-info/requires.txt
--- old/msal-1.10.0/msal.egg-info/requires.txt  2021-03-08 21:46:32.000000000 
+0100
+++ new/msal-1.12.0/msal.egg-info/requires.txt  2021-05-19 22:28:13.000000000 
+0200
@@ -1,3 +1,6 @@
 requests<3,>=2.0.0
 PyJWT[crypto]<3,>=1.0.0
 cryptography<4,>=0.6
+
+[:python_version < "3.3"]
+mock
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.10.0/setup.py new/msal-1.12.0/setup.py
--- old/msal-1.10.0/setup.py    2021-03-08 21:46:19.000000000 +0100
+++ new/msal-1.12.0/setup.py    2021-05-19 22:28:06.000000000 +0200
@@ -58,11 +58,11 @@
         'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
         'License :: OSI Approved :: MIT License',
         'Operating System :: OS Independent',
     ],
@@ -84,6 +84,7 @@
             # We will go with "<4" for now, which is also what our another 
dependency,
             # pyjwt, currently use.
 
+        "mock;python_version<'3.3'",
     ]
 )
 

Reply via email to