From 7de98218ccc7cfb901e29667851624f494a74ab9 Mon Sep 17 00:00:00 2001 From: azure-sdk Date: Wed, 9 Sep 2026 02:35:11 +0000 Subject: [PATCH 01/16] Configurations: 'specification/communication/data-plane/Identity/tspconfig.yaml', API Version: 2026-09-23, SDK Release Type: stable, and CommitSHA: '1893171028aed5a757fc78c2d4aa439c14df4afc' in SpecRepo: 'https://github.com/Azure/azure-rest-api-specs' Pipeline run: https://dev.azure.com/azure-sdk/internal/_build/results?buildId=6802367 Refer to https://eng.ms/docs/products/azure-developer-experience/develop/sdk-release/sdk-release-prerequisites to prepare for SDK release. --- .../azure-communication-identity/CHANGELOG.md | 10 +- .../azure-communication-identity/MANIFEST.in | 6 +- .../_metadata.json | 11 + .../azure-communication-identity/api.md | 2 + .../api.metadata.yml | 4 + .../apiview-properties.json | 37 + .../azure/__init__.py | 2 +- .../azure/communication/__init__.py | 2 +- .../azure/communication/identity/__init__.py | 48 +- .../communication/identity/_api_versions.py | 17 - .../_communication_identity_client.py | 227 --- .../identity/_generated/__init__.py | 19 +- .../identity/_generated/_client.py | 81 +- .../identity/_generated/_configuration.py | 28 +- .../identity/_generated/_patch.py | 38 +- .../_utils}/__init__.py | 7 +- .../identity/_generated/_utils/model_base.py | 1787 +++++++++++++++++ .../serialization.py} | 870 ++++---- .../__init__.py => _generated/_version.py} | 12 +- .../identity/_generated/aio/__init__.py | 16 +- .../identity/_generated/aio/_client.py | 83 +- .../identity/_generated/aio/_configuration.py | 28 +- .../identity/_generated/aio/_patch.py | 38 +- .../_generated/aio/operations/__init__.py | 20 +- .../_generated/aio/operations/_operations.py | 940 +++++++-- .../_generated/aio/operations/_patch.py | 12 +- .../identity/_generated/models/__init__.py | 43 +- .../identity/_generated/models/_enums.py | 13 +- .../identity/_generated/models/_models.py | 476 +++-- .../identity/_generated/models/_patch.py | 12 +- .../_generated/operations/__init__.py | 20 +- .../_generated/operations/_operations.py | 1053 ++++++++-- .../identity/_generated/operations/_patch.py | 12 +- .../identity/_generated/py.typed | 1 - .../identity/_generated/types.py | 93 + .../identity/_shared/auth_policy_utils.py | 53 - .../_shared/entra_token_guard_policy.py | 30 - .../_shared/entra_token_guard_policy_async.py | 30 - .../communication/identity/_shared/models.py | 522 ----- .../communication/identity/_shared/policy.py | 118 -- .../identity/_shared/token_exchange.py | 78 - .../identity/_shared/token_exchange_async.py | 82 - .../identity/_shared/token_utils.py | 91 - .../identity/_shared/user_credential.py | 213 -- .../identity/_shared/user_credential_async.py | 225 --- .../communication/identity/_shared/utils.py | 89 - .../identity/_shared/utils_async.py | 31 - .../azure/communication/identity/_utils.py | 20 - .../azure/communication/identity/_version.py | 10 +- .../_communication_identity_client_async.py | 224 --- .../azure/communication/identity/py.typed | 1 + .../pyproject.toml | 80 +- .../samples/identity_samples.py | 3 +- .../samples/identity_samples_async.py | 3 +- .../sdk_packaging.toml | 7 - .../_shared/communication_service_preparer.py | 1 + .../tests/test_entra_token_guard_policy.py | 13 +- .../test_entra_token_guard_policy_async.py | 4 +- .../tests/test_identifier_raw_id.py | 128 +- .../tests/test_token_exchange.py | 7 +- .../tests/test_token_exchange_async.py | 7 +- .../tests/test_token_utils.py | 12 +- .../tests/test_user_credential.py | 27 +- .../tests/test_user_credential_async.py | 28 +- .../tests/test_utils.py | 12 +- .../tsp-location.yaml | 4 + 66 files changed, 4869 insertions(+), 3352 deletions(-) create mode 100644 sdk/communication/azure-communication-identity/_metadata.json create mode 100644 sdk/communication/azure-communication-identity/api.md create mode 100644 sdk/communication/azure-communication-identity/api.metadata.yml create mode 100644 sdk/communication/azure-communication-identity/apiview-properties.json delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py rename sdk/communication/azure-communication-identity/azure/communication/identity/{_shared => _generated/_utils}/__init__.py (58%) create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py rename sdk/communication/azure-communication-identity/azure/communication/identity/_generated/{_serialization.py => _utils/serialization.py} (72%) rename sdk/communication/azure-communication-identity/azure/communication/identity/{aio/__init__.py => _generated/_version.py} (54%) delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py delete mode 100644 sdk/communication/azure-communication-identity/sdk_packaging.toml create mode 100644 sdk/communication/azure-communication-identity/tsp-location.yaml diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index 26a278bcd6a3..a2fc948b32c1 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -1,15 +1,9 @@ # Release History -## 1.6.0 (Unreleased) +## 1.6.0 (2026-09-09) -### Features Added +skip changelog generation for data-plane package and please add changelog manually. -- Added support for a new communication identifier `TeamsExtensionUserIdentifier`. - - New identifier maps rawIds with format `8:acs:{resourceId}_{tenantId}_{userId}`. - - With this version, rawId starting with `8:acs` may be either `CommunicationUserIdentifier` or new `TeamsExtensionUserIdentifier`. -- Added `IsAnonymous` and `AssertedId` properties to the communication identifier `PhoneNumberIdentifier`. - - `IsAnonymous` is used for anonymous numbers with rawId equals to `4:anonymous`. - - `AssertedId` is used when the same number is used several times in the same call. It contains value after the last underscore `_` character in the phone number. It is null otherwise. ## 1.5.1 (Unreleased) ### Features Added diff --git a/sdk/communication/azure-communication-identity/MANIFEST.in b/sdk/communication/azure-communication-identity/MANIFEST.in index 7595ab04b9be..29aca041172f 100644 --- a/sdk/communication/azure-communication-identity/MANIFEST.in +++ b/sdk/communication/azure-communication-identity/MANIFEST.in @@ -1,7 +1,7 @@ include *.md -include azure/__init__.py -include azure/communication/__init__.py include LICENSE +include azure/communication/identity/py.typed recursive-include tests *.py recursive-include samples *.py *.md -include azure/communication/identity/py.typed +include azure/__init__.py +include azure/communication/__init__.py diff --git a/sdk/communication/azure-communication-identity/_metadata.json b/sdk/communication/azure-communication-identity/_metadata.json new file mode 100644 index 000000000000..08c05f1241b7 --- /dev/null +++ b/sdk/communication/azure-communication-identity/_metadata.json @@ -0,0 +1,11 @@ +{ + "apiVersion": "2026-09-23", + "apiVersions": { + "Azure.Communication.Identity": "2026-09-23" + }, + "commit": "1893171028aed5a757fc78c2d4aa439c14df4afc", + "repository_url": "https://github.com/Azure/azure-rest-api-specs", + "typespec_src": "specification/communication/data-plane/Identity", + "emitterVersion": "0.63.6", + "httpClientPythonVersion": "^0.37.1" +} \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/api.md b/sdk/communication/azure-communication-identity/api.md new file mode 100644 index 000000000000..a75bf12c51ad --- /dev/null +++ b/sdk/communication/azure-communication-identity/api.md @@ -0,0 +1,2 @@ +```py +``` \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/api.metadata.yml b/sdk/communication/azure-communication-identity/api.metadata.yml new file mode 100644 index 000000000000..807cecc73bb7 --- /dev/null +++ b/sdk/communication/azure-communication-identity/api.metadata.yml @@ -0,0 +1,4 @@ +apiMdSha256: 4f975ce96bb34064e16820c7565f93dd884f6d02f76ad1b913a9ed1a8eff2e83 +packageVersion: 1.6.0 +parserVersion: 0.3.31 +pythonVersion: 3.13.15 diff --git a/sdk/communication/azure-communication-identity/apiview-properties.json b/sdk/communication/azure-communication-identity/apiview-properties.json new file mode 100644 index 000000000000..d60c90497e23 --- /dev/null +++ b/sdk/communication/azure-communication-identity/apiview-properties.json @@ -0,0 +1,37 @@ +{ + "CrossLanguagePackageId": "Azure.Communication.Identity", + "CrossLanguageDefinitionId": { + "azure.communication.identity.models.CommunicationError": "Azure.Communication.Identity.CommunicationError", + "azure.communication.identity.models.CommunicationErrorResponse": "Azure.Communication.Identity.CommunicationErrorResponse", + "azure.communication.identity.models.CommunicationIdentity": "Azure.Communication.Identity.CommunicationIdentity", + "azure.communication.identity.models.CommunicationIdentityAccessToken": "Azure.Communication.Identity.CommunicationIdentityAccessToken", + "azure.communication.identity.models.CommunicationIdentityAccessTokenRequest": "Azure.Communication.Identity.CommunicationIdentityAccessTokenRequest", + "azure.communication.identity.models.CommunicationIdentityAccessTokenResult": "Azure.Communication.Identity.CommunicationIdentityAccessTokenResult", + "azure.communication.identity.models.CommunicationIdentityCreateRequest": "Azure.Communication.Identity.CommunicationIdentityCreateRequest", + "azure.communication.identity.models.TeamsExtensionAssignmentCreateOrUpdateRequest": "Azure.Communication.Identity.TeamsExtensionAssignmentCreateOrUpdateRequest", + "azure.communication.identity.models.TeamsExtensionAssignmentResponse": "Azure.Communication.Identity.TeamsExtensionAssignmentResponse", + "azure.communication.identity.models.TeamsExtensionExchangeTokenRequest": "Azure.Communication.Identity.TeamsExtensionExchangeTokenRequest", + "azure.communication.identity.models.TeamsUserExchangeTokenRequest": "Azure.Communication.Identity.TeamsUserExchangeTokenRequest", + "azure.communication.identity.models.CommunicationTokenScope": "Azure.Communication.Identity.CommunicationIdentityTokenScope", + "azure.communication.identity.models.TeamsExtensionPrincipalType": "Azure.Communication.Identity.TeamsExtensionPrincipalType", + "azure.communication.identity.operations.IdentityOperationsOperations.create": "Azure.Communication.Identity.IdentityOperations.create", + "azure.communication.identity.aio.operations.IdentityOperationsOperations.create": "Azure.Communication.Identity.IdentityOperations.create", + "azure.communication.identity.operations.IdentityOperationsOperations.delete": "Azure.Communication.Identity.IdentityOperations.delete", + "azure.communication.identity.aio.operations.IdentityOperationsOperations.delete": "Azure.Communication.Identity.IdentityOperations.delete", + "azure.communication.identity.operations.IdentityOperationsOperations.revoke_access_tokens": "Azure.Communication.Identity.IdentityOperations.revokeAccessTokens", + "azure.communication.identity.aio.operations.IdentityOperationsOperations.revoke_access_tokens": "Azure.Communication.Identity.IdentityOperations.revokeAccessTokens", + "azure.communication.identity.operations.IdentityOperationsOperations.issue_access_token": "Azure.Communication.Identity.IdentityOperations.issueAccessToken", + "azure.communication.identity.aio.operations.IdentityOperationsOperations.issue_access_token": "Azure.Communication.Identity.IdentityOperations.issueAccessToken", + "azure.communication.identity.operations.TeamsUserOperationsOperations.exchange_teams_user_access_token": "Azure.Communication.Identity.TeamsUserOperations.exchangeTeamsUserAccessToken", + "azure.communication.identity.aio.operations.TeamsUserOperationsOperations.exchange_teams_user_access_token": "Azure.Communication.Identity.TeamsUserOperations.exchangeTeamsUserAccessToken", + "azure.communication.identity.operations.TeamsExtensionOperationsOperations.exchange_token": "Azure.Communication.Identity.TeamsExtensionOperations.exchangeToken", + "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.exchange_token": "Azure.Communication.Identity.TeamsExtensionOperations.exchangeToken", + "azure.communication.identity.operations.TeamsExtensionOperationsOperations.get_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.getAssignment", + "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.get_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.getAssignment", + "azure.communication.identity.operations.TeamsExtensionOperationsOperations.upsert_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.upsertAssignment", + "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.upsert_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.upsertAssignment", + "azure.communication.identity.operations.TeamsExtensionOperationsOperations.delete_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.deleteAssignment", + "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.delete_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.deleteAssignment" + }, + "CrossLanguageVersion": "6be142630ab4" +} \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/azure/__init__.py b/sdk/communication/azure-communication-identity/azure/__init__.py index 8db66d3d0f0f..d55ccad1f573 100644 --- a/sdk/communication/azure-communication-identity/azure/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/__init__.py @@ -1 +1 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/communication/azure-communication-identity/azure/communication/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/__init__.py index 8db66d3d0f0f..d55ccad1f573 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/__init__.py @@ -1 +1 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py index c7ad2d71dfa7..d55ccad1f573 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py @@ -1,47 +1 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from ._communication_identity_client import CommunicationIdentityClient -from ._generated.models import CommunicationTokenScope - -from ._shared.models import ( - CommunicationCloudEnvironment, - CommunicationIdentifier, - CommunicationIdentifierKind, - CommunicationUserIdentifier, - CommunicationUserProperties, - identifier_from_raw_id, - MicrosoftTeamsAppIdentifier, - MicrosoftTeamsAppProperties, - MicrosoftTeamsUserIdentifier, - MicrosoftTeamsUserProperties, - PhoneNumberIdentifier, - PhoneNumberProperties, - UnknownIdentifier, - TeamsExtensionUserIdentifier, - TeamsExtensionUserProperties, -) - -__all__ = [ - "CommunicationIdentityClient", - # from _identity - "CommunicationTokenScope", - # from _shared - "CommunicationCloudEnvironment", - "CommunicationIdentifier", - "CommunicationIdentifierKind", - "CommunicationUserIdentifier", - "CommunicationUserProperties", - "identifier_from_raw_id", - "MicrosoftTeamsAppIdentifier", - "MicrosoftTeamsAppProperties", - "MicrosoftTeamsUserIdentifier", - "MicrosoftTeamsUserProperties", - "PhoneNumberIdentifier", - "PhoneNumberProperties", - "UnknownIdentifier", - "TeamsExtensionUserIdentifier", - "TeamsExtensionUserProperties", -] +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py deleted file mode 100644 index 5e6ef85d3a83..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py +++ /dev/null @@ -1,17 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -from enum import Enum -from azure.core import CaseInsensitiveEnumMeta - - -class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): - V2021_03_07 = "2021-03-07" - V2022_06_01 = "2022-06-01" - V2022_10_01 = "2022-10-01" - V2023_10_01 = "2023-10-01" - - -DEFAULT_VERSION = ApiVersion.V2023_10_01 diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py deleted file mode 100644 index ea003e7c564e..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ /dev/null @@ -1,227 +0,0 @@ -# coding=utf-8 -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -from datetime import timedelta -from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union -from azure.core.tracing.decorator import distributed_trace -from azure.core.credentials import AccessToken -from ._generated._client import ( - CommunicationIdentityClient as CommunicationIdentityClientGen, -) -from ._shared.auth_policy_utils import get_authentication_policy -from ._shared.utils import parse_connection_str -from ._shared.models import CommunicationUserIdentifier -from ._version import SDK_MONIKER -from ._api_versions import DEFAULT_VERSION -from ._utils import convert_timedelta_to_mins -from ._generated.models import CommunicationTokenScope - -if TYPE_CHECKING: - from azure.core.credentials import TokenCredential, AzureKeyCredential - - -class CommunicationIdentityClient(object): - """Azure Communication Services Identity client. - - :param str endpoint: - The endpoint url for Azure Communication Service resource. - :param Union[TokenCredential, AzureKeyCredential] credential: - The credential we use to authenticate against the service. - :keyword api_version: Azure Communication Identity API version. - Default value is "2022-10-01". Note that overriding this default value may result in unsupported behavior. - :paramtype api_version: str - - .. admonition:: Example: - - .. literalinclude:: ../samples/identity_samples.py - :language: python - """ - - def __init__( - self, - endpoint: str, - credential, # type: Union[TokenCredential, AzureKeyCredential] - **kwargs # type: Any - ) -> None: - try: - if not endpoint.lower().startswith("http"): - endpoint = "https://" + endpoint - except AttributeError as err: - raise ValueError("Account URL must be a string.") from err - - if not credential: - raise ValueError("You need to provide account shared key to authenticate.") - self._endpoint = endpoint - self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) - self._identity_service_client = CommunicationIdentityClientGen( - self._endpoint, - api_version=self._api_version, - authentication_policy=get_authentication_policy(endpoint, credential), - sdk_moniker=SDK_MONIKER, - **kwargs - ) - - @classmethod - def from_connection_string( - cls, - conn_str: str, - **kwargs: Any - ) -> "CommunicationIdentityClient": - """Create CommunicationIdentityClient from a Connection String. - - :param str conn_str: A connection string to an Azure Communication Service resource. - :returns: Instance of CommunicationIdentityClient. - :rtype: ~azure.communication.identity.CommunicationIdentityClient - """ - endpoint, access_key = parse_connection_str(conn_str) - - # There is logic downstream in method `get_authentication_policy` to handle string credential. - # Marking this as type: ignore to resolve mypy warning. - return cls(endpoint, access_key, **kwargs) # type: ignore - - @distributed_trace - def create_user(self, **kwargs) -> CommunicationUserIdentifier: - """create a single Communication user - - :return: CommunicationUserIdentifier - :rtype: ~azure.communication.identity.CommunicationUserIdentifier - """ - identity_access_token = self._identity_service_client.communication_identity.create(**kwargs) - - return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) - - @distributed_trace - def create_user_and_token( - self, - scopes: List[Union[str, CommunicationTokenScope]], *, - token_expires_in: Optional[timedelta] = None, - **kwargs: Any - ) -> Tuple[CommunicationUserIdentifier, AccessToken]: - """Create a single Communication user with an identity token. - - :param scopes: List of scopes to be added to the token. - :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] - :keyword token_expires_in: Custom validity period of the Communication Identity access token - within [1, 24] hours range. If not provided, the default value of 24 hours will be used. - :paramtype token_expires_in: ~datetime.timedelta - :return: A tuple of a CommunicationUserIdentifier and a AccessToken. - :rtype: - tuple of (~azure.communication.identity.CommunicationUserIdentifier, ~azure.core.credentials.AccessToken) - """ - request_body = { - "createTokenWithScopes": scopes, - "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), - } - identity_access_token = self._identity_service_client.communication_identity.create( - body=request_body, **kwargs # type: ignore - ) - - user_identifier = CommunicationUserIdentifier( - identity_access_token.identity.id, raw_id=identity_access_token.identity.id - ) - access_token = AccessToken( - identity_access_token.access_token.token, - identity_access_token.access_token.expires_on, - ) - - return user_identifier, access_token - - @distributed_trace - def delete_user( - self, - user: CommunicationUserIdentifier, - **kwargs: Any - ) -> None: - """Triggers revocation event for user and deletes all its data. - - :param user: Azure Communication User to delete - :type user: ~azure.communication.identity.CommunicationUserIdentifier - :return: None - :rtype: None - """ - self._identity_service_client.communication_identity.delete(user.properties["id"], **kwargs) - - @distributed_trace - def get_token( - self, - user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], *, - token_expires_in: Optional[timedelta] = None, - **kwargs: Any - ) -> AccessToken: - """Generates a new token for an identity. - - :param user: Azure Communication User - :type user: ~azure.communication.identity.CommunicationUserIdentifier - :param scopes: List of scopes to be added to the token. - :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] - :keyword token_expires_in: Custom validity period of the Communication Identity access token - within [1, 24] hours range. If not provided, the default value of 24 hours will be used. - :paramtype token_expires_in: ~datetime.timedelta - :return: AccessToken - :rtype: ~azure.core.credentials.AccessToken - """ - request_body = { - "scopes": scopes, - "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), - } - - access_token = self._identity_service_client.communication_identity.issue_access_token( - user.properties["id"], body=request_body, **kwargs # type: ignore - ) - - return AccessToken(access_token.token, access_token.expires_on) - - @distributed_trace - def revoke_tokens( - self, - user: CommunicationUserIdentifier, - **kwargs: Any - ) -> None: - """Schedule revocation of all tokens of an identity. - - :param user: Azure Communication User. - :type user: ~azure.communication.identity.CommunicationUserIdentifier. - :return: None - :rtype: None - """ - return self._identity_service_client.communication_identity.revoke_access_tokens( - user.properties["id"] if user else None, **kwargs # type: ignore - ) - - @distributed_trace - def get_token_for_teams_user( - self, - aad_token: str, - client_id: str, - user_object_id: str, - **kwargs: Any - ) -> AccessToken: - """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. - - :param aad_token: an Azure AD access token of a Teams User. - :type aad_token: str - :param client_id: a Client ID of an Azure AD application to be verified against - the appId claim in the Azure AD access token. - :type client_id: str - :param user_object_id: an Object ID of an Azure AD user (Teams User) to be verified against - the OID claim in the Azure AD access token. - :type user_object_id: str - :return: AccessToken - :rtype: ~azure.core.credentials.AccessToken - """ - - request_body = { - "token": aad_token, - "appId": client_id, - "userId": user_object_id, - } - - access_token = self._identity_service_client.communication_identity.exchange_teams_user_access_token( - body=request_body, **kwargs # type: ignore - ) - - return AccessToken(access_token.token, access_token.expires_on) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py index 7d62ac6effd1..d4c9c7017d4c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py @@ -2,22 +2,31 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +# pylint: disable=wrong-import-position -from ._client import CommunicationIdentityClient +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._patch import * # pylint: disable=unused-wildcard-import + +from ._client import IdentityClient # type: ignore +from ._version import VERSION + +__version__ = VERSION try: from ._patch import __all__ as _patch_all - from ._patch import * # pylint: disable=unused-wildcard-import + from ._patch import * except ImportError: _patch_all = [] from ._patch import patch_sdk as _patch_sdk __all__ = [ - "CommunicationIdentityClient", + "IdentityClient", ] -__all__.extend([p for p in _patch_all if p not in __all__]) +__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py index 45e7da9919ec..dd116886cecc 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py @@ -2,33 +2,47 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any from azure.core import PipelineClient +from azure.core.pipeline import policies from azure.core.rest import HttpRequest, HttpResponse -from . import models as _models -from ._configuration import CommunicationIdentityClientConfiguration -from ._serialization import Deserializer, Serializer -from .operations import CommunicationIdentityOperations +from ._configuration import IdentityClientConfiguration +from ._utils.serialization import Deserializer, Serializer +from .operations import IdentityOperationsOperations, TeamsExtensionOperationsOperations, TeamsUserOperationsOperations +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore -class CommunicationIdentityClient: # pylint: disable=client-accepts-api-version-keyword + +class IdentityClient: # pylint: disable=docstring-keyword-should-match-keyword-only """Azure Communication Identity Service. - :ivar communication_identity: CommunicationIdentityOperations operations - :vartype communication_identity: - azure.communication.identity.operations.CommunicationIdentityOperations + :ivar identity_operations: IdentityOperationsOperations operations + :vartype identity_operations: + azure.communication.identity.operations.IdentityOperationsOperations + :ivar teams_user_operations: TeamsUserOperationsOperations operations + :vartype teams_user_operations: + azure.communication.identity.operations.TeamsUserOperationsOperations + :ivar teams_extension_operations: TeamsExtensionOperationsOperations operations + :vartype teams_extension_operations: + azure.communication.identity.operations.TeamsExtensionOperationsOperations :param endpoint: The communication resource, for example - https://my-resource.communication.azure.com. Required. + `https://my-resource.communication.azure.com `_. + Required. :type endpoint: str - :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" + and None. Default value is None. If not set, the operation's default API version will be used. + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ @@ -36,18 +50,41 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self, endpoint: str, **kwargs: Any ) -> None: _endpoint = "{endpoint}" - self._config = CommunicationIdentityClientConfiguration(endpoint=endpoint, **kwargs) - self._client: PipelineClient = PipelineClient(base_url=_endpoint, config=self._config, **kwargs) - - client_models = {k: v for k, v in _models.__dict__.items() if isinstance(v, type)} - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) + self._config = IdentityClientConfiguration(endpoint=endpoint, **kwargs) + + _policies = kwargs.pop("policies", None) + if _policies is None: + _policies = [ + policies.RequestIdPolicy(**kwargs), + self._config.headers_policy, + self._config.user_agent_policy, + self._config.proxy_policy, + policies.ContentDecodePolicy(**kwargs), + self._config.redirect_policy, + self._config.retry_policy, + self._config.authentication_policy, + self._config.custom_hook_policy, + self._config.logging_policy, + policies.DistributedTracingPolicy(**kwargs), + policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None, + self._config.http_logging_policy, + ] + self._client: PipelineClient = PipelineClient(base_url=_endpoint, policies=_policies, **kwargs) + + self._serialize = Serializer() + self._deserialize = Deserializer() self._serialize.client_side_validation = False - self.communication_identity = CommunicationIdentityOperations( + self.identity_operations = IdentityOperationsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.teams_user_operations = TeamsUserOperationsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.teams_extension_operations = TeamsExtensionOperationsOperations( self._client, self._config, self._serialize, self._deserialize ) - def send_request(self, request: HttpRequest, **kwargs: Any) -> HttpResponse: + def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: """Runs the network request through the client's chained policies. >>> from azure.core.rest import HttpRequest @@ -71,12 +108,12 @@ def send_request(self, request: HttpRequest, **kwargs: Any) -> HttpResponse: } request_copy.url = self._client.format_url(request_copy.url, **path_format_arguments) - return self._client.send_request(request_copy, **kwargs) + return self._client.send_request(request_copy, stream=stream, **kwargs) # type: ignore def close(self) -> None: self._client.close() - def __enter__(self) -> "CommunicationIdentityClient": + def __enter__(self) -> Self: self._client.__enter__() return self diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py index 3ba79d8d9d97..8e224fc5e630 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py @@ -1,45 +1,45 @@ +# pylint: disable=line-too-long,useless-suppression # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from typing import Any -from azure.core.configuration import Configuration from azure.core.pipeline import policies -VERSION = "unknown" +from ._version import VERSION -class CommunicationIdentityClientConfiguration( # pylint: disable=too-many-instance-attributes,name-too-long - Configuration -): - """Configuration for CommunicationIdentityClient. +class IdentityClientConfiguration: # pylint: disable=too-many-instance-attributes,docstring-keyword-should-match-keyword-only + """Configuration for IdentityClient. Note that all parameters used to create this instance are saved as instance attributes. :param endpoint: The communication resource, for example - https://my-resource.communication.azure.com. Required. + `https://my-resource.communication.azure.com `_. + Required. :type endpoint: str - :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" + and None. Default value is None. If not set, the operation's default API version will be used. + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, **kwargs: Any) -> None: - super(CommunicationIdentityClientConfiguration, self).__init__(**kwargs) - api_version: str = kwargs.pop("api_version", "2023-10-01") + api_version: str = kwargs.pop("api_version", "2026-09-23") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") self.endpoint = endpoint self.api_version = api_version - kwargs.setdefault("sdk_moniker", "communicationidentityclient/{}".format(VERSION)) + kwargs.setdefault("sdk_moniker", "communication-identity/{}".format(VERSION)) + self.polling_interval = kwargs.get("polling_interval", 30) self._configure(**kwargs) def _configure(self, **kwargs: Any) -> None: @@ -48,7 +48,7 @@ def _configure(self, **kwargs: Any) -> None: self.proxy_policy = kwargs.get("proxy_policy") or policies.ProxyPolicy(**kwargs) self.logging_policy = kwargs.get("logging_policy") or policies.NetworkTraceLoggingPolicy(**kwargs) self.http_logging_policy = kwargs.get("http_logging_policy") or policies.HttpLoggingPolicy(**kwargs) - self.retry_policy = kwargs.get("retry_policy") or policies.RetryPolicy(**kwargs) self.custom_hook_policy = kwargs.get("custom_hook_policy") or policies.CustomHookPolicy(**kwargs) self.redirect_policy = kwargs.get("redirect_policy") or policies.RedirectPolicy(**kwargs) + self.retry_policy = kwargs.get("retry_policy") or policies.RetryPolicy(**kwargs) self.authentication_policy = kwargs.get("authentication_policy") diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py index 17dbc073e01b..ea765788358a 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py @@ -1,32 +1,20 @@ # coding=utf-8 # -------------------------------------------------------------------------- -# # Copyright (c) Microsoft Corporation. All rights reserved. -# -# The MIT License (MIT) -# -# 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. -# +# Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------- +"""Customize generated code here. + +Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize +""" + +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level -# This file is used for handwritten extensions to the generated code. Example: -# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/customize_code/how-to-patch-sdk-code.md def patch_sdk(): - pass + """Do not remove from this file. + + `patch_sdk` is a last resort escape hatch that allows you to do customizations + you can't accomplish using the techniques described in + https://aka.ms/azsdk/python/dpcodegen/python/customize + """ diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py similarity index 58% rename from sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py rename to sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py index 5b396cd202e8..8026245c2abc 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py @@ -1,5 +1,6 @@ -# ------------------------------------------------------------------------- +# -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py new file mode 100644 index 000000000000..35d5fc024978 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py @@ -0,0 +1,1787 @@ +# pylint: disable=line-too-long,useless-suppression,too-many-lines +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# -------------------------------------------------------------------------- +# pylint: disable=protected-access, broad-except + +import copy +import calendar +import decimal +import functools +import sys +import logging +import base64 +import re +import typing +import enum +import email.utils +from datetime import datetime, date, time, timedelta, timezone +from json import JSONEncoder +import xml.etree.ElementTree as ET +from collections.abc import MutableMapping +import isodate +from azure.core.exceptions import DeserializationError +from azure.core import CaseInsensitiveEnumMeta +from azure.core.pipeline import PipelineResponse +from azure.core.serialization import _Null + +from azure.core.rest import HttpResponse + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +_LOGGER = logging.getLogger(__name__) + +__all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] + +TZ_UTC = timezone.utc +_T = typing.TypeVar("_T") +_NONE_TYPE = type(None) + + +def _timedelta_as_isostr(td: timedelta) -> str: + """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S' + + Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython + + :param timedelta td: The timedelta to convert + :rtype: str + :return: ISO8601 version of this timedelta + """ + + # Split seconds to larger units + seconds = td.total_seconds() + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + + days, hours, minutes = list(map(int, (days, hours, minutes))) + seconds = round(seconds, 6) + + # Build date + date_str = "" + if days: + date_str = "%sD" % days + + if hours or minutes or seconds: + # Build time + time_str = "T" + + # Hours + bigger_exists = date_str or hours + if bigger_exists: + time_str += "{:02}H".format(hours) + + # Minutes + bigger_exists = bigger_exists or minutes + if bigger_exists: + time_str += "{:02}M".format(minutes) + + # Seconds + try: + if seconds.is_integer(): + seconds_string = "{:02}".format(int(seconds)) + else: + # 9 chars long w/ leading 0, 6 digits after decimal + seconds_string = "%09.6f" % seconds + # Remove trailing zeros + seconds_string = seconds_string.rstrip("0") + except AttributeError: # int.is_integer() raises + seconds_string = "{:02}".format(seconds) + + time_str += "{}S".format(seconds_string) + else: + time_str = "" + + return "P" + date_str + time_str + + +def _serialize_bytes(o, format: typing.Optional[str] = None) -> str: + encoded = base64.b64encode(o).decode() + if format == "base64url": + return encoded.strip("=").replace("+", "-").replace("/", "_") + return encoded + + +def _serialize_duration(td: timedelta, format: typing.Optional[str] = None): + """Serialize a timedelta to its wire representation. + + For the ``seconds``/``milliseconds`` encodings the value is converted to a + numeric value, otherwise it falls back to an ISO 8601 duration string. + + :param timedelta td: The timedelta to serialize. + :param str format: The duration encoding format. + :rtype: int or float or str + :return: serialized duration + """ + seconds = td.total_seconds() + if format == "duration-seconds-int": + return int(seconds) + if format == "duration-seconds-float": + return seconds + if format == "duration-milliseconds-int": + return int(seconds * 1000) + if format == "duration-milliseconds-float": + return seconds * 1000 + return _timedelta_as_isostr(td) + + +def _serialize_datetime(o, format: typing.Optional[str] = None): + if hasattr(o, "year") and hasattr(o, "hour"): + if format == "rfc7231": + return email.utils.format_datetime(o, usegmt=True) + if format == "unix-timestamp": + return int(calendar.timegm(o.utctimetuple())) + + # astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set) + if not o.tzinfo: + iso_formatted = o.replace(tzinfo=TZ_UTC).isoformat() + else: + iso_formatted = o.astimezone(TZ_UTC).isoformat() + # Replace the trailing "+00:00" UTC offset with "Z" (RFC 3339: https://www.ietf.org/rfc/rfc3339.txt) + return iso_formatted.replace("+00:00", "Z") + # Next try datetime.date or datetime.time + return o.isoformat() + + +def _is_readonly(p): + try: + return p._visibility == ["read"] + except AttributeError: + return False + + +class SdkJSONEncoder(JSONEncoder): + """A JSON encoder that's capable of serializing datetime objects and bytes. + + :param args: Additional positional arguments passed to the base ``JSONEncoder``. + :type args: typing.Any + :keyword exclude_readonly: Whether to exclude readonly properties. Defaults to False. + :paramtype exclude_readonly: bool + :keyword format: The format to use for serialization. Defaults to None. + :paramtype format: typing.Optional[str] + """ + + def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs): + super().__init__(*args, **kwargs) + self.exclude_readonly = exclude_readonly + self.format = format + + def default(self, o): # pylint: disable=too-many-return-statements + if _is_model(o): + if self.exclude_readonly: + readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] + return {k: v for k, v in o.items() if k not in readonly_props} + return dict(o.items()) + try: + return super(SdkJSONEncoder, self).default(o) + except TypeError: + if isinstance(o, _Null): + return None + if isinstance(o, decimal.Decimal): + return float(o) + if isinstance(o, (bytes, bytearray)): + return _serialize_bytes(o, self.format) + try: + # First try datetime.datetime + return _serialize_datetime(o, self.format) + except AttributeError: + pass + # Last, try datetime.timedelta + try: + return _timedelta_as_isostr(o) + except AttributeError: + # This will be raised when it hits value.total_seconds in the method above + pass + return super(SdkJSONEncoder, self).default(o) + + +_VALID_DATE = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" + r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") +_VALID_RFC7231 = re.compile( + r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s" + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT" +) + +_ARRAY_ENCODE_MAPPING = { + "pipeDelimited": "|", + "spaceDelimited": " ", + "commaDelimited": ",", + "newlineDelimited": "\n", +} + + +def _deserialize_array_encoded(delimit: str, attr): + if isinstance(attr, str): + if attr == "": + return [] + return attr.split(delimit) + return attr + + +def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: + """Deserialize ISO-8601 formatted string into Datetime object. + + :param str attr: response string to be deserialized. + :rtype: ~datetime.datetime + :returns: The datetime object from that input + """ + if isinstance(attr, datetime): + # i'm already deserialized + return attr + attr = attr.upper() + match = _VALID_DATE.match(attr) + if not match: + raise ValueError("Invalid datetime string: " + attr) + + check_decimal = attr.split(".") + if len(check_decimal) > 1: + decimal_str = "" + for digit in check_decimal[1]: + if digit.isdigit(): + decimal_str += digit + else: + break + if len(decimal_str) > 6: + attr = attr.replace(decimal_str, decimal_str[0:6]) + + date_obj = isodate.parse_datetime(attr) + test_utc = date_obj.utctimetuple() + if test_utc.tm_year > 9999 or test_utc.tm_year < 1: + raise OverflowError("Hit max or min date") + return date_obj # type: ignore[no-any-return] + + +def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime: + """Deserialize RFC7231 formatted string into Datetime object. + + :param str attr: response string to be deserialized. + :rtype: ~datetime.datetime + :returns: The datetime object from that input + """ + if isinstance(attr, datetime): + # i'm already deserialized + return attr + match = _VALID_RFC7231.match(attr) + if not match: + raise ValueError("Invalid datetime string: " + attr) + + return email.utils.parsedate_to_datetime(attr) + + +def _deserialize_datetime_unix_timestamp(attr: typing.Union[float, datetime]) -> datetime: + """Deserialize unix timestamp into Datetime object. + + :param str attr: response string to be deserialized. + :rtype: ~datetime.datetime + :returns: The datetime object from that input + """ + if isinstance(attr, datetime): + # i'm already deserialized + return attr + return datetime.fromtimestamp(attr, TZ_UTC) + + +def _deserialize_date(attr: typing.Union[str, date]) -> date: + """Deserialize ISO-8601 formatted string into Date object. + :param str attr: response string to be deserialized. + :rtype: date + :returns: The date object from that input + """ + # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. + if isinstance(attr, date): + return attr + return isodate.parse_date(attr, defaultmonth=None, defaultday=None) # type: ignore + + +def _deserialize_time(attr: typing.Union[str, time]) -> time: + """Deserialize ISO-8601 formatted string into time object. + + :param str attr: response string to be deserialized. + :rtype: datetime.time + :returns: The time object from that input + """ + if isinstance(attr, time): + return attr + return isodate.parse_time(attr) # type: ignore[no-any-return] + + +def _deserialize_bytes(attr): + if isinstance(attr, (bytes, bytearray)): + return attr + return bytes(base64.b64decode(attr)) + + +def _deserialize_bytes_base64(attr): + if isinstance(attr, (bytes, bytearray)): + return attr + padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore + attr = attr + padding # type: ignore + encoded = attr.replace("-", "+").replace("_", "/") + return bytes(base64.b64decode(encoded)) + + +def _deserialize_duration(attr): + if isinstance(attr, timedelta): + return attr + return isodate.parse_duration(attr) + + +def _deserialize_duration_numeric(attr, unit): + if isinstance(attr, timedelta): + return attr + return timedelta(**{unit: float(attr)}) + + +def _deserialize_decimal(attr): + if isinstance(attr, decimal.Decimal): + return attr + return decimal.Decimal(str(attr)) + + +def _deserialize_int_as_str(attr): + if isinstance(attr, int): + return attr + return int(attr) + + +def _deserialize_bool_as_str(attr): + if isinstance(attr, bool): + return attr + return attr.lower() == "true" + + +_DESERIALIZE_MAPPING = { + datetime: _deserialize_datetime, + date: _deserialize_date, + time: _deserialize_time, + bytes: _deserialize_bytes, + bytearray: _deserialize_bytes, + timedelta: _deserialize_duration, + typing.Any: lambda x: x, + decimal.Decimal: _deserialize_decimal, +} + +_DESERIALIZE_MAPPING_WITHFORMAT = { + "rfc3339": _deserialize_datetime, + "rfc7231": _deserialize_datetime_rfc7231, + "unix-timestamp": _deserialize_datetime_unix_timestamp, + "base64": _deserialize_bytes, + "base64url": _deserialize_bytes_base64, + "duration-seconds-int": functools.partial(_deserialize_duration_numeric, unit="seconds"), + "duration-seconds-float": functools.partial(_deserialize_duration_numeric, unit="seconds"), + "duration-milliseconds-int": functools.partial(_deserialize_duration_numeric, unit="milliseconds"), + "duration-milliseconds-float": functools.partial(_deserialize_duration_numeric, unit="milliseconds"), +} + + +def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None): + if annotation is int and rf and rf._format == "str": + return _deserialize_int_as_str + if annotation is bool and rf and rf._format == "str": + return _deserialize_bool_as_str + if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING: + return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format]) + if rf and rf._format: + return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) + return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore + + +def _get_type_alias_type(module_name: str, alias_name: str): + types = { + k: v + for k, v in sys.modules[module_name].__dict__.items() + if isinstance(v, typing._GenericAlias) # type: ignore + } + if alias_name not in types: + return alias_name + return types[alias_name] + + +def _get_model(module_name: str, model_name: str): + models = {k: v for k, v in sys.modules[module_name].__dict__.items() if isinstance(v, type)} + module_end = module_name.rsplit(".", 1)[0] + models.update({k: v for k, v in sys.modules[module_end].__dict__.items() if isinstance(v, type)}) + if isinstance(model_name, str): + model_name = model_name.split(".")[-1] + if model_name not in models: + return model_name + return models[model_name] + + +_UNSET = object() + + +class _MyMutableMapping(MutableMapping[str, typing.Any]): + def __init__(self, data: dict[str, typing.Any]) -> None: + self._data = data + + def __contains__(self, key: typing.Any) -> bool: + return key in self._data + + def __getitem__(self, key: str) -> typing.Any: + # If this key has been deserialized (for mutable types), we need to handle serialization + if hasattr(self, "_attr_to_rest_field"): + cache_attr = f"_deserialized_{key}" + if hasattr(self, cache_attr): + rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key) + if rf: + value = self._data.get(key) + if isinstance(value, (dict, list, set)): + # For mutable types, serialize and return + # But also update _data with serialized form and clear flag + # so mutations via this returned value affect _data + serialized = _serialize(value, rf._format) + # If serialized form is same type (no transformation needed), + # return _data directly so mutations work + if isinstance(serialized, type(value)) and serialized == value: + return self._data.get(key) + # Otherwise return serialized copy and clear flag + try: + object.__delattr__(self, cache_attr) + except AttributeError: + pass + # Store serialized form back + self._data[key] = serialized + return serialized + return self._data.__getitem__(key) + + def __setitem__(self, key: str, value: typing.Any) -> None: + # Clear any cached deserialized value when setting through dictionary access + cache_attr = f"_deserialized_{key}" + try: + object.__delattr__(self, cache_attr) + except AttributeError: + pass + self._data.__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + self._data.__delitem__(key) + + def __iter__(self) -> typing.Iterator[typing.Any]: + return self._data.__iter__() + + def __len__(self) -> int: + return self._data.__len__() + + def __ne__(self, other: typing.Any) -> bool: + return not self.__eq__(other) + + def keys(self) -> typing.KeysView[str]: + """ + :returns: a set-like object providing a view on the mapping's keys + :rtype: ~typing.KeysView + """ + return self._data.keys() + + def values(self) -> typing.ValuesView[typing.Any]: + """ + :returns: an object providing a view on the mapping's values + :rtype: ~typing.ValuesView + """ + return self._data.values() + + def items(self) -> typing.ItemsView[str, typing.Any]: + """ + :returns: a set-like object providing a view on the mapping's items + :rtype: ~typing.ItemsView + """ + return self._data.items() + + def get(self, key: str, default: typing.Any = None) -> typing.Any: + """ + Get the value for key if key is in the dictionary, else default. + :param str key: The key to look up. + :param any default: The value to return if key is not in the dictionary. Defaults to None + :returns: The value for key if key is in the dictionary, else default. + :rtype: any + """ + try: + return self[key] + except KeyError: + return default + + @typing.overload + def pop(self, key: str) -> typing.Any: ... # pylint: disable=arguments-differ + + @typing.overload + def pop(self, key: str, default: _T) -> _T: ... # pylint: disable=signature-differs + + @typing.overload + def pop(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs + + def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any: + """ + Removes specified key and return the corresponding value. + :param str key: The key to pop. + :param any default: The value to return if key is not in the dictionary + :returns: The value corresponding to the key. + :rtype: any + :raises KeyError: If key is not found and default is not given. + """ + if default is _UNSET: + return self._data.pop(key) + return self._data.pop(key, default) + + def popitem(self) -> tuple[str, typing.Any]: + """ + Removes and returns some (key, value) pair + :returns: The (key, value) pair. + :rtype: tuple + :raises KeyError: if the dictionary is empty. + """ + return self._data.popitem() + + def clear(self) -> None: + """ + Remove all items from the dictionary. + """ + self._data.clear() + + def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: # pylint: disable=arguments-differ + """ + Update the dictionary from a mapping or an iterable of key-value pairs. + :param any args: Either a mapping object or an iterable of key-value pairs. + """ + self._data.update(*args, **kwargs) + + @typing.overload + def setdefault(self, key: str, default: None = None) -> None: ... + + @typing.overload + def setdefault(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs + + def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any: + """ + Return the value for key if key is in the dictionary; otherwise set the key to + default and return default. + :param str key: The key to look up. + :param any default: The value to set if key is not in the dictionary + :returns: The value for key if key is in the dictionary, else default. + :rtype: any + """ + if default is _UNSET: + return self._data.setdefault(key) + return self._data.setdefault(key, default) + + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, _MyMutableMapping): + return self._data == other._data + try: + other_model = self.__class__(other) + except Exception: + return False + return self._data == other_model._data + + def __repr__(self) -> str: + return str(self._data) + + +def _is_model(obj: typing.Any) -> bool: + return getattr(obj, "_is_model", False) + + +def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements + if isinstance(o, list): + if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o): + return _ARRAY_ENCODE_MAPPING[format].join(o) + return [_serialize(x, format) for x in o] + if isinstance(o, dict): + return {k: _serialize(v, format) for k, v in o.items()} + if isinstance(o, set): + return {_serialize(x, format) for x in o} + if isinstance(o, tuple): + return tuple(_serialize(x, format) for x in o) + if isinstance(o, (bytes, bytearray)): + return _serialize_bytes(o, format) + if isinstance(o, decimal.Decimal): + return float(o) + if isinstance(o, enum.Enum): + return o.value + if isinstance(o, int): + if format == "str": + return str(o) + return o + try: + # First try datetime.datetime + return _serialize_datetime(o, format) + except AttributeError: + pass + # Last, try datetime.timedelta + try: + return _serialize_duration(o, format) + except AttributeError: + # This will be raised when it hits value.total_seconds in the method above + pass + return o + + +def _get_rest_field(attr_to_rest_field: dict[str, "_RestField"], rest_name: str) -> typing.Optional["_RestField"]: + try: + return next(rf for rf in attr_to_rest_field.values() if rf._rest_name == rest_name) + except StopIteration: + return None + + +def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any: + if not rf: + return _serialize(value, None) + if rf._is_multipart_file_input: + return value + if rf._is_model: + return _deserialize(rf._type, value) + if isinstance(value, ET.Element): + value = _deserialize(rf._type, value) + return _serialize(value, rf._format) + + +# ============================================================================ +# Fast-path scalar deserializer functions for rest_field(deserializer=...) +# These are referenced from rest_field declarations to bypass the generic +# _deserialize -> _deserialize_with_callable chain. +# Only simple/primitive types — no models or container types. +# ============================================================================ + + +def _xml_deser_str(value): + if isinstance(value, ET.Element): + return value.text or "" + return str(value) if value is not None else None + + +def _xml_deser_int(value): + if isinstance(value, ET.Element): + return int(value.text) if value.text else None + return int(value) if value is not None else None + + +def _xml_deser_float(value): + if isinstance(value, ET.Element): + return float(value.text) if value.text else None + return float(value) if value is not None else None + + +def _xml_deser_bool(value): + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + if text in (True, False): + return text + return text.lower() == "true" + + +# pylint: disable=docstring-missing-param +def _xml_deser_bytes(value): + """Deserialize bytes from XML (base64).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_bytes(text) + + +def _xml_deser_bytes_base64url(value): + """Deserialize bytes from XML (base64url).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_bytes_base64(text) + + +def _xml_deser_datetime(value): + """Deserialize a datetime from XML (ISO 8601 / rfc3339).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_datetime(text) + + +def _xml_deser_datetime_rfc7231(value): + """Deserialize a datetime from XML (RFC7231 format).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_datetime_rfc7231(text) + + +def _xml_deser_datetime_unix_timestamp(value): + """Deserialize a datetime from XML (Unix timestamp).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_datetime_unix_timestamp(float(text)) + + +def _xml_deser_date(value): + """Deserialize a date from XML (ISO 8601).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_date(text) + + +def _xml_deser_time(value): + """Deserialize a time from XML (ISO 8601).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_time(text) + + +def _xml_deser_duration(value): + """Deserialize a timedelta from XML (ISO 8601 duration).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_duration(text) + + +def _xml_deser_decimal(value): + """Deserialize a Decimal from XML.""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_decimal(text) + + +def _xml_deser_enum_or_str(enum_cls, value): + """Deserialize a Union[EnumType, str] from XML.""" + text = value.text if isinstance(value, ET.Element) else value + if text is None: + return None + try: + return enum_cls(text) + except ValueError: + return text + + +def _extract_xml_model_type(rf_type): + """Extract the concrete Model class from a resolved rf._type partial chain. + + Unwraps ``Optional[Model]`` and ``_deserialize_model(Model, ...)`` + wrappers. Only handles Model and Optional[Model] — other composite + types (List, Dict, Union, etc.) return None and fall through to the + generic ``_deserialize`` path at runtime. + """ + if rf_type is None: + return None + if isinstance(rf_type, type) and _is_model(rf_type): + return rf_type + if not isinstance(rf_type, functools.partial): + return None + func = rf_type.func + args = rf_type.args + if func is _deserialize_with_optional and args: + return _extract_xml_model_type(args[0]) + if func is _deserialize_model and args: + cls = args[0] + return cls if isinstance(cls, type) and _is_model(cls) else None + return None + + +def _build_xml_field_plan( # pylint: disable=docstring-missing-return, docstring-missing-rtype, unused-variable + cls, attr_to_rest_field: dict +) -> list: + """Build a precomputed XML field plan for fast _init_from_xml iteration. + + Called once per model class in __new__. Returns a list of tuples: + (rest_name, xml_name, kind, deser, rf_type, is_optional, items_name) + + kind: 0=wrapped, 1=attribute, 2=unwrapped, 3=text + + For Model and Optional[Model] fields that lack a scalar + ``_deserializer``, this function precomputes the Model class as the + deserializer so ``_init_from_xml`` can call ``ModelClass(element)`` + directly instead of going through the expensive + ``_get_deserialize_callable_from_annotation`` chain at runtime. + """ + model_meta = getattr(cls, "_xml", {}) + model_ns = model_meta.get("ns") or model_meta.get("namespace") + plan = [] + + for rf in attr_to_rest_field.values(): + prop_meta = getattr(rf, "_xml", {}) + deser = rf._deserializer + + xml_name = prop_meta.get("name", rf._rest_name) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + + is_optional = rf._is_optional + + # For Model / Optional[Model] fields without a scalar deserializer, + # precompute the Model class as the deserializer. + if deser is None and rf._type is not None: + model_cls = _extract_xml_model_type(rf._type) + if model_cls is not None: + deser = model_cls + + if prop_meta.get("attribute", False): + plan.append((rf._rest_name, xml_name, 1, deser, rf._type, is_optional, None)) + elif prop_meta.get("unwrapped", False): + items_name = prop_meta.get("itemsName") + if items_name: + items_ns = prop_meta.get("itemsNs") + if items_ns is not None: + xml_ns = items_ns + if xml_ns: + items_name = "{" + xml_ns + "}" + items_name + else: + items_name = xml_name + plan.append((rf._rest_name, xml_name, 2, deser, rf._type, is_optional, items_name)) + elif prop_meta.get("text", False): + plan.append((rf._rest_name, xml_name, 3, deser, rf._type, is_optional, None)) + else: + plan.append((rf._rest_name, xml_name, 0, deser, rf._type, is_optional, None)) + + return plan + + +# pylint: enable=docstring-missing-param +class Model(_MyMutableMapping): + _is_model = True + # label whether current class's _attr_to_rest_field has been calculated + # could not see _attr_to_rest_field directly because subclass inherits it from parent class + _calculated: set[str] = set() + + def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: + class_name = self.__class__.__name__ + if len(args) > 1: + raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") + dict_to_pass: dict[str, typing.Any] = {} + if args: + if isinstance(args[0], ET.Element): + dict_to_pass.update(self._init_from_xml(args[0])) + else: + dict_to_pass.update( + {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()} + ) + else: + non_attr_kwargs = [k for k in kwargs if k not in self._attr_to_rest_field] + if non_attr_kwargs: + # actual type errors only throw the first wrong keyword arg they see, so following that. + raise TypeError(f"{class_name}.__init__() got an unexpected keyword argument '{non_attr_kwargs[0]}'") + dict_to_pass.update( + { + self._attr_to_rest_field[k]._rest_name: _create_value(self._attr_to_rest_field[k], v) + for k, v in kwargs.items() + if v is not None + } + ) + # Apply client default values for fields the caller didn't set so that + # defaults are part of `_data` and therefore included during serialization. + for rf in self._attr_to_rest_field.values(): + if rf._default is _UNSET: + continue + if rf._rest_name in dict_to_pass: + continue + dict_to_pass[rf._rest_name] = _create_value(rf, rf._default) + super().__init__(dict_to_pass) + + def _init_from_xml( # pylint: disable=too-many-branches, too-many-statements + self, element: ET.Element + ) -> dict[str, typing.Any]: + """Deserialize an XML element into a dict mapping rest field names to values. + + :param ET.Element element: The XML element to deserialize from. + :returns: A dictionary of rest_name to deserialized value pairs. + :rtype: dict + """ + result: dict[str, typing.Any] = {} + existed_attr_keys: list[str] = [] + + field_plan = getattr(self, "_xml_field_plan", None) + if field_plan: + for rest_name, xml_name, kind, deser, rf_type, is_optional, items_name in field_plan: + if kind == 0: # wrapped element (most common) + item = element.find(xml_name) + if item is not None: + existed_attr_keys.append(xml_name) + if deser: + result[rest_name] = deser(item) + else: + result[rest_name] = _deserialize(rf_type, item) + elif kind == 1: # attribute + attr_val = element.get(xml_name) + if attr_val is not None: + existed_attr_keys.append(xml_name) + if deser: + result[rest_name] = deser(attr_val) + else: + result[rest_name] = attr_val + elif kind == 2: # unwrapped array + items = element.findall(items_name) # pyright: ignore + if len(items) > 0: + existed_attr_keys.append(items_name) + if deser: + result[rest_name] = deser(items) + else: + result[rest_name] = _deserialize(rf_type, items) + elif not is_optional: + existed_attr_keys.append(items_name) + result[rest_name] = [] + elif kind == 3: # text + if element.text is not None: + if deser: + result[rest_name] = deser(element.text) + else: + result[rest_name] = element.text + else: + model_meta = getattr(self, "_xml", {}) + for rf in self._attr_to_rest_field.values(): + prop_meta = getattr(rf, "_xml", {}) + xml_name = prop_meta.get("name", rf._rest_name) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + + # attribute + if prop_meta.get("attribute", False) and element.get(xml_name) is not None: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name)) + continue + + # unwrapped element is array + if prop_meta.get("unwrapped", False): + _items_name = prop_meta.get("itemsName") + if _items_name: + xml_name = _items_name + _items_ns = prop_meta.get("itemsNs") + if _items_ns is not None: + xml_ns = _items_ns + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + items = element.findall(xml_name) # pyright: ignore + if len(items) > 0: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, items) + elif not rf._is_optional: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = [] + continue + + # text element is primitive type + if prop_meta.get("text", False): + if element.text is not None: + result[rf._rest_name] = _deserialize(rf._type, element.text) + continue + + # wrapped element could be normal property or array + item = element.find(xml_name) + if item is not None: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, item) + + # rest thing is additional properties + for e in element: + if e.tag not in existed_attr_keys: + result[e.tag] = _convert_element(e) + + return result + + def copy(self) -> "Model": + return Model(self.__dict__) + + def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: + if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated: + # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping', + # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object' + mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order + attr_to_rest_field: dict[str, _RestField] = { # map attribute name to rest_field property + k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type") + } + annotations = { + k: v + for mro_class in mros + if hasattr(mro_class, "__annotations__") + for k, v in mro_class.__annotations__.items() + } + for attr, rf in attr_to_rest_field.items(): + rf._module = cls.__module__ + if not rf._type: + rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None)) + if not rf._rest_name_input: + rf._rest_name_input = attr + cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items()) + # Build XML field plan for fast _init_from_xml (only for XML models) + if getattr(cls, "_xml", None): + cls._xml_field_plan = _build_xml_field_plan(cls, attr_to_rest_field) + cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") + + return super().__new__(cls) + + def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None: + for base in cls.__bases__: + if hasattr(base, "__mapping__"): + base.__mapping__[discriminator or cls.__name__] = cls # type: ignore + + @classmethod + def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: + for v in cls.__dict__.values(): + if isinstance(v, _RestField) and v._is_discriminator and v._rest_name not in exist_discriminators: + return v + return None + + @classmethod + def _deserialize(cls, data, exist_discriminators): + if not hasattr(cls, "__mapping__"): + return cls(data) + discriminator = cls._get_discriminator(exist_discriminators) + if discriminator is None: + return cls(data) + exist_discriminators.append(discriminator._rest_name) + if isinstance(data, ET.Element): + model_meta = getattr(cls, "_xml", {}) + prop_meta = getattr(discriminator, "_xml", {}) + xml_name = prop_meta.get("name", discriminator._rest_name) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + + if data.get(xml_name) is not None: + discriminator_value = data.get(xml_name) + else: + discriminator_value = data.find(xml_name).text # pyright: ignore + else: + discriminator_value = data.get(discriminator._rest_name) + mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore # pylint: disable=no-member + return mapped_cls._deserialize(data, exist_discriminators) + + def as_dict(self, *, exclude_readonly: bool = False) -> dict[str, typing.Any]: + """Return a dict that can be turned into json using json.dump. + + :keyword bool exclude_readonly: Whether to remove the readonly properties. + :returns: A dict JSON compatible object + :rtype: dict + """ + + result = {} + readonly_props = [] + if exclude_readonly: + readonly_props = [p._rest_name for p in self._attr_to_rest_field.values() if _is_readonly(p)] + for k, v in self.items(): + if exclude_readonly and k in readonly_props: # pyright: ignore + continue + is_multipart_file_input = False + try: + is_multipart_file_input = next( + rf for rf in self._attr_to_rest_field.values() if rf._rest_name == k + )._is_multipart_file_input + except StopIteration: + pass + result[k] = v if is_multipart_file_input else Model._as_dict_value(v, exclude_readonly=exclude_readonly) + return result + + @staticmethod + def _as_dict_value(v: typing.Any, exclude_readonly: bool = False) -> typing.Any: + if v is None or isinstance(v, _Null): + return None + if isinstance(v, (list, tuple, set)): + return type(v)(Model._as_dict_value(x, exclude_readonly=exclude_readonly) for x in v) + if isinstance(v, dict): + return {dk: Model._as_dict_value(dv, exclude_readonly=exclude_readonly) for dk, dv in v.items()} + return v.as_dict(exclude_readonly=exclude_readonly) if hasattr(v, "as_dict") else v + + +def _deserialize_model(model_deserializer: typing.Optional[typing.Callable], obj): + if _is_model(obj): + return obj + return _deserialize(model_deserializer, obj) + + +def _deserialize_with_optional(if_obj_deserializer: typing.Optional[typing.Callable], obj): + if obj is None: + return obj + return _deserialize_with_callable(if_obj_deserializer, obj) + + +def _deserialize_with_union(deserializers, obj): + for deserializer in deserializers: + try: + return _deserialize(deserializer, obj) + except DeserializationError: + pass + raise DeserializationError() + + +def _deserialize_dict( + value_deserializer: typing.Optional[typing.Callable], + module: typing.Optional[str], + obj: dict[typing.Any, typing.Any], +): + if obj is None: + return obj + if isinstance(obj, ET.Element): + obj = {child.tag: child for child in obj} + return {k: _deserialize(value_deserializer, v, module) for k, v in obj.items()} + + +def _deserialize_multiple_sequence( + entry_deserializers: list[typing.Optional[typing.Callable]], + module: typing.Optional[str], + obj, +): + if obj is None: + return obj + return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers)) + + +def _is_array_encoded_deserializer(deserializer: functools.partial) -> bool: + return ( + isinstance(deserializer, functools.partial) + and isinstance(deserializer.args[0], functools.partial) + and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable + ) + + +def _deserialize_sequence( + deserializer: typing.Optional[typing.Callable], + module: typing.Optional[str], + obj, +): + if obj is None: + return obj + if isinstance(obj, ET.Element): + obj = list(obj) + + # encoded string may be deserialized to sequence + if isinstance(obj, str) and isinstance(deserializer, functools.partial): + # for list[str] + if _is_array_encoded_deserializer(deserializer): + return deserializer(obj) + + # for list[Union[...]] + if isinstance(deserializer.args[0], list): + for sub_deserializer in deserializer.args[0]: + if _is_array_encoded_deserializer(sub_deserializer): + return sub_deserializer(obj) + + return type(obj)(_deserialize(deserializer, entry, module) for entry in obj) + + +def _sorted_annotations(types: list[typing.Any]) -> list[typing.Any]: + return sorted( + types, + key=lambda x: hasattr(x, "__name__") and x.__name__.lower() in ("str", "float", "int", "bool"), + ) + + +def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-statements, too-many-branches + annotation: typing.Any, + module: typing.Optional[str], + rf: typing.Optional["_RestField"] = None, +) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: + if not annotation: + return None + + # is it a type alias? + if isinstance(annotation, str): + if module is not None: + annotation = _get_type_alias_type(module, annotation) + + # is it a forward ref / in quotes? + if isinstance(annotation, (str, typing.ForwardRef)): + try: + model_name = annotation.__forward_arg__ # type: ignore + except AttributeError: + model_name = annotation + if module is not None: + annotation = _get_model(module, model_name) # type: ignore + + try: + if module and _is_model(annotation): + if rf: + rf._is_model = True + + return functools.partial(_deserialize_model, annotation) # pyright: ignore + except Exception: + pass + + # is it a literal? + try: + if annotation.__origin__ is typing.Literal: # pyright: ignore + return None + except AttributeError: + pass + + # is it optional? + try: + if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore + if rf: + rf._is_optional = True + if len(annotation.__args__) <= 2: # pyright: ignore + if_obj_deserializer = _get_deserialize_callable_from_annotation( + next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore + ) + + return functools.partial(_deserialize_with_optional, if_obj_deserializer) + # the type is Optional[Union[...]], we need to remove the None type from the Union + annotation_copy = copy.copy(annotation) + annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a is not _NONE_TYPE] # pyright: ignore + return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) + except AttributeError: + pass + + # is it union? + if getattr(annotation, "__origin__", None) is typing.Union: + # initial ordering is we make `string` the last deserialization option, because it is often them most generic + deserializers = [ + _get_deserialize_callable_from_annotation(arg, module, rf) + for arg in _sorted_annotations(annotation.__args__) # pyright: ignore + ] + + return functools.partial(_deserialize_with_union, deserializers) + + try: + annotation_name = ( + annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore + ) + if annotation_name.lower() == "dict": + value_deserializer = _get_deserialize_callable_from_annotation( + annotation.__args__[1], module, rf # pyright: ignore + ) + + return functools.partial( + _deserialize_dict, + value_deserializer, + module, + ) + except (AttributeError, IndexError): + pass + try: + annotation_name = ( + annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore + ) + if annotation_name.lower() in ["list", "set", "tuple", "sequence"]: + if len(annotation.__args__) > 1: # pyright: ignore + entry_deserializers = [ + _get_deserialize_callable_from_annotation(dt, module, rf) + for dt in annotation.__args__ # pyright: ignore + ] + return functools.partial(_deserialize_multiple_sequence, entry_deserializers, module) + deserializer = _get_deserialize_callable_from_annotation( + annotation.__args__[0], module, rf # pyright: ignore + ) + + return functools.partial(_deserialize_sequence, deserializer, module) + except (TypeError, IndexError, AttributeError, SyntaxError): + pass + + def _deserialize_default( + deserializer, + obj, + ): + if obj is None: + return obj + try: + return _deserialize_with_callable(deserializer, obj) + except Exception: + pass + return obj + + if get_deserializer(annotation, rf): + return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) + + return functools.partial(_deserialize_default, annotation) + + +def _deserialize_with_callable( + deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]], + value: typing.Any, +): # pylint: disable=too-many-return-statements + try: + if value is None or isinstance(value, _Null): + return None + if isinstance(value, ET.Element): + if deserializer is str: + return value.text or "" + if deserializer is int: + return int(value.text) if value.text else None + if deserializer is float: + return float(value.text) if value.text else None + if deserializer is bool: + return value.text == "true" if value.text else None + if deserializer and deserializer in _DESERIALIZE_MAPPING.values(): + return deserializer(value.text) if value.text else None + if deserializer and deserializer in _DESERIALIZE_MAPPING_WITHFORMAT.values(): + return deserializer(value.text) if value.text else None + if deserializer is None: + return value + if deserializer in [int, float, bool]: + return deserializer(value) + if isinstance(deserializer, CaseInsensitiveEnumMeta): + try: + return deserializer(value.text if isinstance(value, ET.Element) else value) + except ValueError: + # for unknown value, return raw value + return value.text if isinstance(value, ET.Element) else value + if isinstance(deserializer, type) and issubclass(deserializer, Model): + return deserializer._deserialize(value, []) + return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value) + except Exception as e: + raise DeserializationError() from e + + +def _deserialize( + deserializer: typing.Any, + value: typing.Any, + module: typing.Optional[str] = None, + rf: typing.Optional["_RestField"] = None, + format: typing.Optional[str] = None, +) -> typing.Any: + if isinstance(value, PipelineResponse): + value = value.http_response.json() + if rf is None and format: + rf = _RestField(format=format) + if not isinstance(deserializer, functools.partial): + deserializer = _get_deserialize_callable_from_annotation(deserializer, module, rf) + return _deserialize_with_callable(deserializer, value) + + +def _failsafe_deserialize( + deserializer: typing.Any, + response: HttpResponse, + module: typing.Optional[str] = None, + rf: typing.Optional["_RestField"] = None, + format: typing.Optional[str] = None, +) -> typing.Any: + try: + return _deserialize(deserializer, response.json(), module, rf, format) + except Exception: # pylint: disable=broad-except + _LOGGER.warning( + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True + ) + return None + + +def _failsafe_deserialize_xml( + deserializer: typing.Any, + response: HttpResponse, +) -> typing.Any: + try: + return _deserialize_xml(deserializer, response.text()) + except Exception: # pylint: disable=broad-except + _LOGGER.warning( + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True + ) + return None + + +# pylint: disable=too-many-instance-attributes +class _RestField: + def __init__( + self, + *, + name: typing.Optional[str] = None, + type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin + is_discriminator: bool = False, + visibility: typing.Optional[list[str]] = None, + default: typing.Any = _UNSET, + format: typing.Optional[str] = None, + is_multipart_file_input: bool = False, + xml: typing.Optional[dict[str, typing.Any]] = None, + deserializer: typing.Optional[typing.Callable] = None, + ): + self._type = type + self._rest_name_input = name + self._module: typing.Optional[str] = None + self._is_discriminator = is_discriminator + self._visibility = visibility + self._is_model = False + self._is_optional = False + self._default = default + self._format = format + self._is_multipart_file_input = is_multipart_file_input + self._xml = xml if xml is not None else {} + self._deserializer = deserializer + + @property + def _class_type(self) -> typing.Any: + result = getattr(self._type, "args", [None])[0] + # type may be wrapped by nested functools.partial so we need to check for that + if isinstance(result, functools.partial): + return getattr(result, "args", [None])[0] + return result + + @property + def _rest_name(self) -> str: + if self._rest_name_input is None: + raise ValueError("Rest name was never set") + return self._rest_name_input + + def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin + # by this point, type and rest_name will have a value bc we default + # them in __new__ of the Model class + # Use _data.get() directly to avoid triggering __getitem__ which clears the cache + item = obj._data.get(self._rest_name, _UNSET) + if item is _UNSET: + # Field not set by user; return the client default if one exists, otherwise None + return self._default if self._default is not _UNSET else None + if item is None: + return item + if self._is_model: + return item + + # For mutable types, we want mutations to directly affect _data + # Check if we've already deserialized this value + cache_attr = f"_deserialized_{self._rest_name}" + if hasattr(obj, cache_attr): + # Return the value from _data directly (it's been deserialized in place) + return obj._data.get(self._rest_name) + + # Fast path: use _deserializer directly (avoids _serialize/_deserialize chain) + if self._deserializer: + deserialized = self._deserializer(item) + else: + deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self) + + # For mutable types, store the deserialized value back in _data + # so mutations directly affect _data + if isinstance(deserialized, (dict, list, set)): + obj._data[self._rest_name] = deserialized + object.__setattr__(obj, cache_attr, True) # Mark as deserialized + return deserialized + + return deserialized + + def __set__(self, obj: Model, value) -> None: + # Clear the cached deserialized object when setting a new value + cache_attr = f"_deserialized_{self._rest_name}" + if hasattr(obj, cache_attr): + object.__delattr__(obj, cache_attr) + + if value is None: + # we want to wipe out entries if users set attr to None + try: + obj.__delitem__(self._rest_name) + except KeyError: + pass + return + if self._is_model: + if not _is_model(value): + value = _deserialize(self._type, value) + obj.__setitem__(self._rest_name, value) + return + obj.__setitem__(self._rest_name, _serialize(value, self._format)) + + def _get_deserialize_callable_from_annotation( + self, annotation: typing.Any + ) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: + return _get_deserialize_callable_from_annotation(annotation, self._module, self) + + +def rest_field( + *, + name: typing.Optional[str] = None, + type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin + visibility: typing.Optional[list[str]] = None, + default: typing.Any = _UNSET, + format: typing.Optional[str] = None, + is_multipart_file_input: bool = False, + xml: typing.Optional[dict[str, typing.Any]] = None, + deserializer: typing.Optional[typing.Callable] = None, +) -> typing.Any: + return _RestField( + name=name, + type=type, + visibility=visibility, + default=default, + format=format, + is_multipart_file_input=is_multipart_file_input, + xml=xml, + deserializer=deserializer, + ) + + +def rest_discriminator( + *, + name: typing.Optional[str] = None, + type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin + visibility: typing.Optional[list[str]] = None, + xml: typing.Optional[dict[str, typing.Any]] = None, +) -> typing.Any: + return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml) + + +def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: + """Serialize a model to XML. + + :param Model model: The model to serialize. + :param bool exclude_readonly: Whether to exclude readonly properties. + :returns: The XML representation of the model. + :rtype: str + """ + return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore + + +def _get_xml_ns(meta: dict[str, typing.Any]) -> typing.Optional[str]: + """Return the XML namespace from a metadata dict, checking both 'ns' (old-style) and 'namespace' (DPG) keys. + + :param dict meta: The metadata dictionary to extract namespace from. + :returns: The namespace string if 'ns' or 'namespace' key is present, None otherwise. + :rtype: str or None + """ + ns = meta.get("ns") + if ns is None: + ns = meta.get("namespace") + return ns + + +def _resolve_xml_ns( + prop_meta: dict[str, typing.Any], model_meta: typing.Optional[dict[str, typing.Any]] = None +) -> typing.Optional[str]: + """Resolve XML namespace for a property, falling back to model namespace when appropriate. + + Checks the property metadata first; if no namespace is found and the model does not declare + an explicit prefix, falls back to the model-level namespace. + + :param dict prop_meta: The property metadata dictionary. + :param dict model_meta: The model metadata dictionary, used as fallback. + :returns: The resolved namespace string, or None. + :rtype: str or None + """ + ns = _get_xml_ns(prop_meta) + if ns is None and model_meta is not None and not model_meta.get("prefix"): + ns = _get_xml_ns(model_meta) + return ns + + +def _set_xml_attribute(element: ET.Element, name: str, value: typing.Any, prop_meta: dict[str, typing.Any]) -> None: + """Set an XML attribute on an element, handling namespace prefix registration. + + :param ET.Element element: The element to set the attribute on. + :param str name: The default attribute name (wire name). + :param any value: The attribute value. + :param dict prop_meta: The property metadata dictionary. + """ + xml_name = prop_meta.get("name", name) + _attr_ns = _get_xml_ns(prop_meta) + if _attr_ns: + _attr_prefix = prop_meta.get("prefix") + if _attr_prefix: + _safe_register_namespace(_attr_prefix, _attr_ns) + xml_name = "{" + _attr_ns + "}" + xml_name + element.set(xml_name, _get_primitive_type_value(value)) + + +def _get_element( + o: typing.Any, + exclude_readonly: bool = False, + parent_meta: typing.Optional[dict[str, typing.Any]] = None, + wrapped_element: typing.Optional[ET.Element] = None, +) -> typing.Union[ET.Element, list[ET.Element]]: + if _is_model(o): + model_meta = getattr(o, "_xml", {}) + + # if prop is a model, then use the prop element directly, else generate a wrapper of model + if wrapped_element is None: + # When serializing as an array item (parent_meta is set), check if the parent has an + # explicit itemsName. This ensures correct element names for unwrapped arrays (where + # the element tag is the property/items name, not the model type name). + _items_name = parent_meta.get("itemsName") if parent_meta is not None else None + element_name = _items_name if _items_name else (model_meta.get("name") or o.__class__.__name__) + _model_ns = _get_xml_ns(model_meta) + wrapped_element = _create_xml_element( + element_name, + model_meta.get("prefix"), + _model_ns, + ) + + readonly_props = [] + if exclude_readonly: + readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] + + for k, v in o.items(): + # do not serialize readonly properties + if exclude_readonly and k in readonly_props: + continue + + prop_rest_field = _get_rest_field(o._attr_to_rest_field, k) + if prop_rest_field: + prop_meta = getattr(prop_rest_field, "_xml").copy() + # use the wire name as xml name if no specific name is set + if prop_meta.get("name") is None: + prop_meta["name"] = k + else: + # additional properties will not have rest field, use the wire name as xml name + prop_meta = {"name": k} + + # Propagate model namespace to properties only for old-style "ns"-keyed models. + # DPG-generated models use the "namespace" key and explicitly declare namespace on + # each property that needs it, so propagation is intentionally skipped for them. + if prop_meta.get("ns") is None and model_meta.get("ns"): + prop_meta["ns"] = model_meta.get("ns") + prop_meta["prefix"] = model_meta.get("prefix") + + if prop_meta.get("unwrapped", False): + # unwrapped could only set on array + wrapped_element.extend(_get_element(v, exclude_readonly, prop_meta)) + elif prop_meta.get("text", False): + # text could only set on primitive type + wrapped_element.text = _get_primitive_type_value(v) + elif prop_meta.get("attribute", False): + _set_xml_attribute(wrapped_element, k, v, prop_meta) + else: + # other wrapped prop element + wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) + return wrapped_element + if isinstance(o, list): + return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore + if isinstance(o, dict): + result = [] + _dict_ns = _get_xml_ns(parent_meta) if parent_meta else None + for k, v in o.items(): + result.append( + _get_wrapped_element( + v, + exclude_readonly, + { + "name": k, + "ns": _dict_ns, + "prefix": parent_meta.get("prefix") if parent_meta else None, + }, + ) + ) + return result + + # primitive case need to create element based on parent_meta + if parent_meta: + _items_ns = parent_meta.get("itemsNs") + if _items_ns is None: + _items_ns = _get_xml_ns(parent_meta) + return _get_wrapped_element( + o, + exclude_readonly, + { + "name": parent_meta.get("itemsName", parent_meta.get("name")), + "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")), + "ns": _items_ns, + }, + ) + + raise ValueError("Could not serialize value into xml: " + o) + + +def _get_wrapped_element( + v: typing.Any, + exclude_readonly: bool, + meta: typing.Optional[dict[str, typing.Any]], +) -> ET.Element: + _meta_ns = _get_xml_ns(meta) if meta else None + wrapped_element = _create_xml_element( + meta.get("name") if meta else None, meta.get("prefix") if meta else None, _meta_ns + ) + if isinstance(v, (dict, list)): + wrapped_element.extend(_get_element(v, exclude_readonly, meta)) + elif _is_model(v): + _get_element(v, exclude_readonly, meta, wrapped_element) + else: + wrapped_element.text = _get_primitive_type_value(v) + return wrapped_element # type: ignore[no-any-return] + + +def _get_primitive_type_value(v) -> str: + if v is True: + return "true" + if v is False: + return "false" + if isinstance(v, _Null): + return "" + return str(v) + + +def _safe_register_namespace(prefix: str, ns: str) -> None: + """Register an XML namespace prefix, handling reserved prefix patterns. + + Some prefixes (e.g. 'ns2') match Python's reserved 'ns\\d+' pattern used for + auto-generated prefixes, causing register_namespace to raise ValueError. + Falls back to directly registering in the internal namespace map. + + :param str prefix: The namespace prefix to register. + :param str ns: The namespace URI. + """ + try: + ET.register_namespace(prefix, ns) + except ValueError: + _ns_map = getattr(ET, "_namespace_map", None) + if _ns_map is not None: + _ns_map[ns] = prefix + + +def _create_xml_element( + tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None +) -> ET.Element: + if prefix and ns: + _safe_register_namespace(prefix, ns) + if ns: + return ET.Element("{" + ns + "}" + tag) + return ET.Element(tag) + + +def _deserialize_xml( + deserializer: typing.Any, + value: str, +) -> typing.Any: + element = ET.fromstring(value) # nosec + if _is_model(deserializer): + return deserializer._deserialize(element, []) + return _deserialize(deserializer, element) + + +def _convert_element(e: ET.Element): + # dict case + if len(e.attrib) > 0 or len({child.tag for child in e}) > 1: + dict_result: dict[str, typing.Any] = {} + for child in e: + if dict_result.get(child.tag) is not None: + if isinstance(dict_result[child.tag], list): + dict_result[child.tag].append(_convert_element(child)) + else: + dict_result[child.tag] = [dict_result[child.tag], _convert_element(child)] + else: + dict_result[child.tag] = _convert_element(child) + dict_result.update(e.attrib) + return dict_result + # array case + if len(e) > 0: + array_result: list[typing.Any] = [] + for child in e: + array_result.append(_convert_element(child)) + return array_result + # primitive case + return e.text diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_serialization.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/serialization.py similarity index 72% rename from sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_serialization.py rename to sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/serialization.py index c13084631923..ae08f9d89f74 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_serialization.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/serialization.py @@ -1,30 +1,12 @@ +# pylint: disable=line-too-long,useless-suppression,too-many-lines +# coding=utf-8 # -------------------------------------------------------------------------- -# # Copyright (c) Microsoft Corporation. All rights reserved. -# -# The MIT License (MIT) -# -# 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. -# +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: skip-file # pyright: reportUnnecessaryTypeIgnoreComment=false from base64 import b64decode, b64encode @@ -39,7 +21,6 @@ import sys import codecs from typing import ( - Dict, Any, cast, Optional, @@ -48,11 +29,7 @@ IO, Mapping, Callable, - TypeVar, MutableMapping, - Type, - List, - Mapping, ) try: @@ -63,20 +40,21 @@ import isodate # type: ignore -from azure.core.exceptions import ( - DeserializationError, - SerializationError, - raise_with_traceback, -) -from azure.core.serialization import NULL as AzureCoreNull +from azure.core.exceptions import DeserializationError, SerializationError +from azure.core.serialization import NULL as CoreNull + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self _BOM = codecs.BOM_UTF8.decode(encoding="utf-8") -ModelType = TypeVar("ModelType", bound="Model") JSON = MutableMapping[str, Any] class RawDeserializer: + # Accept "text" because we're open minded people... JSON_REGEXP = re.compile(r"^(application|text)/([a-z+.]+\+)?json$") @@ -94,6 +72,8 @@ def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: :param data: Input, could be bytes or stream (will be decoded with UTF8) or text :type data: str or bytes or IO :param str content_type: The content type. + :return: The deserialized data. + :rtype: object """ if hasattr(data, "read"): # Assume a stream @@ -115,9 +95,10 @@ def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: try: return json.loads(data_as_str) except ValueError as err: - raise DeserializationError("JSON is invalid: {}".format(err), err) + raise DeserializationError("JSON is invalid: {}".format(err), err) from err elif "xml" in (content_type or []): try: + try: if isinstance(data, unicode): # type: ignore # If I'm Python 2.7 and unicode XML will scream if I try a "fromstring" on unicode string @@ -126,7 +107,7 @@ def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: pass return ET.fromstring(data_as_str) # nosec - except ET.ParseError: + except ET.ParseError as err: # It might be because the server has an issue, and returned JSON with # content-type XML.... # So let's try a JSON load, and if it's still broken @@ -145,7 +126,9 @@ def _json_attemp(data): # The function hack is because Py2.7 messes up with exception # context otherwise. _LOGGER.critical("Wasn't XML not JSON, failing") - raise_with_traceback(DeserializationError, "XML is invalid") + raise DeserializationError("XML is invalid") from err + elif content_type.startswith("text/"): + return data_as_str raise DeserializationError("Cannot deserialize content-type: {}".format(content_type)) @classmethod @@ -155,6 +138,11 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], Use bytes and headers to NOT use any requests/aiohttp or whatever specific implementation. Headers will tested for "content-type" + + :param bytes body_bytes: The body of the response. + :param dict headers: The headers of the response. + :returns: The deserialized data. + :rtype: object """ # Try to use content-type from headers if available content_type = None @@ -172,13 +160,6 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], return None -try: - basestring # type: ignore - unicode_str = unicode # type: ignore -except NameError: - basestring = str - unicode_str = str - _LOGGER = logging.getLogger(__name__) try: @@ -186,80 +167,31 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], except NameError: _long_type = int - -class UTC(datetime.tzinfo): - """Time Zone info for handling UTC""" - - def utcoffset(self, dt): - """UTF offset for UTC is 0.""" - return datetime.timedelta(0) - - def tzname(self, dt): - """Timestamp representation.""" - return "Z" - - def dst(self, dt): - """No daylight saving for UTC.""" - return datetime.timedelta(hours=1) - - -try: - from datetime import timezone as _FixedOffset # type: ignore -except ImportError: # Python 2.7 - - class _FixedOffset(datetime.tzinfo): # type: ignore - """Fixed offset in minutes east from UTC. - Copy/pasted from Python doc - :param datetime.timedelta offset: offset in timedelta format - """ - - def __init__(self, offset): - self.__offset = offset - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return str(self.__offset.total_seconds() / 3600) - - def __repr__(self): - return "".format(self.tzname(None)) - - def dst(self, dt): - return datetime.timedelta(0) - - def __getinitargs__(self): - return (self.__offset,) - - -try: - from datetime import timezone - - TZ_UTC = timezone.utc -except ImportError: - TZ_UTC = UTC() # type: ignore +TZ_UTC = datetime.timezone.utc _FLATTEN = re.compile(r"(? None: - self.additional_properties: Dict[str, Any] = {} - for k in kwargs: + self.additional_properties: Optional[dict[str, Any]] = {} + for k in kwargs: # pylint: disable=consider-using-dict-items if k not in self._attribute_map: - _LOGGER.warning( - "%s is not a known attribute of class %s and will be ignored", - k, - self.__class__, - ) + _LOGGER.warning("%s is not a known attribute of class %s and will be ignored", k, self.__class__) elif k in self._validation and self._validation[k].get("readonly", False): - _LOGGER.warning( - "Readonly attribute %s will be ignored in class %s", - k, - self.__class__, - ) + _LOGGER.warning("Readonly attribute %s will be ignored in class %s", k, self.__class__) else: setattr(self, k, kwargs[k]) def __eq__(self, other: Any) -> bool: - """Compare objects by comparing all attributes.""" + """Compare objects by comparing all attributes. + + :param object other: The object to compare + :returns: True if objects are equal + :rtype: bool + """ if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ return False def __ne__(self, other: Any) -> bool: - """Compare objects by comparing all attributes.""" + """Compare objects by comparing all attributes. + + :param object other: The object to compare + :returns: True if objects are not equal + :rtype: bool + """ return not self.__eq__(other) def __str__(self) -> str: @@ -341,20 +282,20 @@ def is_xml_model(cls) -> bool: @classmethod def _create_xml_node(cls): - """Create XML node.""" + """Create XML node. + + :returns: The XML node + :rtype: xml.etree.ElementTree.Element + """ try: xml_map = cls._xml_map # type: ignore except AttributeError: xml_map = {} - return _create_xml_node( - xml_map.get("name", cls.__name__), - xml_map.get("prefix", None), - xml_map.get("ns", None), - ) + return _create_xml_node(xml_map.get("name", cls.__name__), xml_map.get("prefix", None), xml_map.get("ns", None)) def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: - """Return the JSON that would be sent to azure from this model. + """Return the JSON that would be sent to server from this model. This is an alias to `as_dict(full_restapi_key_transformer, keep_readonly=False)`. @@ -365,12 +306,14 @@ def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: :rtype: dict """ serializer = Serializer(self._infer_class_models()) - return serializer._serialize(self, keep_readonly=keep_readonly, **kwargs) + return serializer._serialize( # type: ignore # pylint: disable=protected-access + self, keep_readonly=keep_readonly, **kwargs + ) def as_dict( self, keep_readonly: bool = True, - key_transformer: Callable[[str, Dict[str, Any], Any], Any] = attribute_transformer, + key_transformer: Callable[[str, dict[str, Any], Any], Any] = attribute_transformer, **kwargs: Any ) -> JSON: """Return a dict that can be serialized using json.dump. @@ -399,12 +342,15 @@ def my_key_transformer(key, attr_desc, value): If you want XML serialization, you can pass the kwargs is_xml=True. + :param bool keep_readonly: If you want to serialize the readonly attributes :param function key_transformer: A key transformer function. :returns: A dict JSON compatible object :rtype: dict """ serializer = Serializer(self._infer_class_models()) - return serializer._serialize(self, key_transformer=key_transformer, keep_readonly=keep_readonly, **kwargs) + return serializer._serialize( # type: ignore # pylint: disable=protected-access + self, key_transformer=key_transformer, keep_readonly=keep_readonly, **kwargs + ) @classmethod def _infer_class_models(cls): @@ -414,30 +360,31 @@ def _infer_class_models(cls): client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} if cls.__name__ not in client_models: raise ValueError("Not Autorest generated code") - except Exception: + except Exception: # pylint: disable=broad-exception-caught # Assume it's not Autorest generated (tests?). Add ourselves as dependencies. client_models = {cls.__name__: cls} return client_models @classmethod - def deserialize(cls: Type[ModelType], data: Any, content_type: Optional[str] = None) -> ModelType: + def deserialize(cls, data: Any, content_type: Optional[str] = None) -> Self: """Parse a str using the RestAPI syntax and return a model. :param str data: A str using RestAPI structure. JSON by default. :param str content_type: JSON by default, set application/xml if XML. :returns: An instance of this model - :raises: DeserializationError if something went wrong + :raises DeserializationError: if something went wrong + :rtype: Self """ deserializer = Deserializer(cls._infer_class_models()) - return deserializer(cls.__name__, data, content_type=content_type) + return deserializer(cls.__name__, data, content_type=content_type) # type: ignore @classmethod def from_dict( - cls: Type[ModelType], + cls, data: Any, - key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, + key_extractors: Optional[Callable[[str, dict[str, Any], Any], Any]] = None, content_type: Optional[str] = None, - ) -> ModelType: + ) -> Self: """Parse a dict using given key extractor return a model. By default consider key @@ -445,9 +392,11 @@ def from_dict( and last_rest_key_case_insensitive_extractor) :param dict data: A dict using RestAPI structure + :param function key_extractors: A key extractor function. :param str content_type: JSON by default, set application/xml if XML. :returns: An instance of this model - :raises: DeserializationError if something went wrong + :raises DeserializationError: if something went wrong + :rtype: Self """ deserializer = Deserializer(cls._infer_class_models()) deserializer.key_extractors = ( # type: ignore @@ -459,7 +408,7 @@ def from_dict( if key_extractors is None else key_extractors ) - return deserializer(cls.__name__, data, content_type=content_type) + return deserializer(cls.__name__, data, content_type=content_type) # type: ignore @classmethod def _flatten_subtype(cls, key, objects): @@ -467,21 +416,25 @@ def _flatten_subtype(cls, key, objects): return {} result = dict(cls._subtype_map[key]) for valuetype in cls._subtype_map[key].values(): - result.update(objects[valuetype]._flatten_subtype(key, objects)) + result |= objects[valuetype]._flatten_subtype(key, objects) # pylint: disable=protected-access return result @classmethod def _classify(cls, response, objects): """Check the class _subtype_map for any child classes. We want to ignore any inherited _subtype_maps. - Remove the polymorphic key from the initial data. + + :param dict response: The initial data + :param dict objects: The class objects + :returns: The class to be used + :rtype: class """ for subtype_key in cls.__dict__.get("_subtype_map", {}).keys(): subtype_value = None if not isinstance(response, ET.Element): rest_api_response_key = cls._get_rest_key_parts(subtype_key)[-1] - subtype_value = response.pop(rest_api_response_key, None) or response.pop(subtype_key, None) + subtype_value = response.get(rest_api_response_key, None) or response.get(subtype_key, None) else: subtype_value = xml_key_extractor(subtype_key, cls._attribute_map[subtype_key], response) if subtype_value: @@ -500,11 +453,7 @@ def _classify(cls, response, objects): ) break else: - _LOGGER.warning( - "Discriminator %s is absent or null, use base class %s.", - subtype_key, - cls.__name__, - ) + _LOGGER.warning("Discriminator %s is absent or null, use base class %s.", subtype_key, cls.__name__) break return cls @@ -524,12 +473,18 @@ def _decode_attribute_map_key(key): inside the received data. :param str key: A key string from the generated code + :returns: The decoded key + :rtype: str """ return key.replace("\\.", ".") -class Serializer(object): - """Request object model serializer.""" +class Serializer: # pylint: disable=too-many-public-methods + """Request object model serializer. + + :param classes: Mapping of model names to model types, used to resolve models during serialization. + :type classes: typing.Optional[typing.Mapping[str, type]] + """ basic_types = {str: "str", int: "int", bool: "bool", float: "float"} @@ -563,12 +518,16 @@ class Serializer(object): "multiple": lambda x, y: x % y != 0, } - def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): + def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: self.serialize_type = { "iso-8601": Serializer.serialize_iso, "rfc-1123": Serializer.serialize_rfc, "unix-time": Serializer.serialize_unix, "duration": Serializer.serialize_duration, + "duration-seconds-int": Serializer.serialize_duration_seconds_int, + "duration-seconds-float": Serializer.serialize_duration_seconds_float, + "duration-milliseconds-int": Serializer.serialize_duration_milliseconds_int, + "duration-milliseconds-float": Serializer.serialize_duration_milliseconds_float, "date": Serializer.serialize_date, "time": Serializer.serialize_time, "decimal": Serializer.serialize_decimal, @@ -579,17 +538,20 @@ def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): "[]": self.serialize_iter, "{}": self.serialize_dict, } - self.dependencies: Dict[str, Type[ModelType]] = dict(classes) if classes else {} + self.dependencies: dict[str, type] = dict(classes) if classes else {} self.key_transformer = full_restapi_key_transformer self.client_side_validation = True - def _serialize(self, target_obj, data_type=None, **kwargs): + def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, too-many-statements, too-many-locals + self, target_obj, data_type=None, **kwargs + ): """Serialize data into a string according to type. - :param target_obj: The data to be serialized. + :param object target_obj: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str, dict - :raises: SerializationError if serialization fails. + :raises SerializationError: if serialization fails. + :returns: The serialized data. """ key_transformer = kwargs.get("key_transformer", self.key_transformer) keep_readonly = kwargs.get("keep_readonly", False) @@ -615,19 +577,22 @@ def _serialize(self, target_obj, data_type=None, **kwargs): serialized = {} if is_xml_model_serialization: - serialized = target_obj._create_xml_node() + serialized = target_obj._create_xml_node() # pylint: disable=protected-access try: - attributes = target_obj._attribute_map + attributes = target_obj._attribute_map # pylint: disable=protected-access for attr, attr_desc in attributes.items(): attr_name = attr - if not keep_readonly and target_obj._validation.get(attr_name, {}).get("readonly", False): + if not keep_readonly and target_obj._validation.get( # pylint: disable=protected-access + attr_name, {} + ).get("readonly", False): continue if attr_name == "additional_properties" and attr_desc["key"] == "": if target_obj.additional_properties is not None: - serialized.update(target_obj.additional_properties) + serialized |= target_obj.additional_properties continue try: + orig_attr = getattr(target_obj, attr) if is_xml_model_serialization: pass # Don't provide "transformer" for XML for now. Keep "orig_attr" @@ -655,7 +620,8 @@ def _serialize(self, target_obj, data_type=None, **kwargs): if isinstance(new_attr, list): serialized.extend(new_attr) # type: ignore elif isinstance(new_attr, ET.Element): - # If the down XML has no XML/Name, we MUST replace the tag with the local tag. But keeping the namespaces. + # If the down XML has no XML/Name, + # we MUST replace the tag with the local tag. But keeping the namespaces. if "name" not in getattr(orig_attr, "_xml_map", {}): splitted_tag = new_attr.tag.split("}") if len(splitted_tag) == 2: # Namespace @@ -666,7 +632,7 @@ def _serialize(self, target_obj, data_type=None, **kwargs): else: # That's a basic type # Integrate namespace if necessary local_node = _create_xml_node(xml_name, xml_prefix, xml_ns) - local_node.text = unicode_str(new_attr) + local_node.text = str(new_attr) serialized.append(local_node) # type: ignore else: # JSON for k in reversed(keys): # type: ignore @@ -685,18 +651,18 @@ def _serialize(self, target_obj, data_type=None, **kwargs): except (AttributeError, KeyError, TypeError) as err: msg = "Attribute {} in object {} cannot be serialized.\n{}".format(attr_name, class_name, str(target_obj)) - raise_with_traceback(SerializationError, msg, err) - else: - return serialized + raise SerializationError(msg) from err + return serialized def body(self, data, data_type, **kwargs): """Serialize data intended for a request body. - :param data: The data to be serialized. + :param object data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: dict - :raises: SerializationError if serialization fails. - :raises: ValueError if data is None + :raises SerializationError: if serialization fails. + :raises ValueError: if data is None + :returns: The serialized request body """ # Just in case this is a dict @@ -725,20 +691,22 @@ def body(self, data, data_type, **kwargs): attribute_key_case_insensitive_extractor, last_rest_key_case_insensitive_extractor, ] - data = deserializer._deserialize(data_type, data) + data = deserializer._deserialize(data_type, data) # pylint: disable=protected-access except DeserializationError as err: - raise_with_traceback(SerializationError, "Unable to build a model: " + str(err), err) + raise SerializationError("Unable to build a model: " + str(err)) from err return self._serialize(data, data_type, **kwargs) def url(self, name, data, data_type, **kwargs): """Serialize data intended for a URL path. - :param data: The data to be serialized. + :param str name: The name of the URL path parameter. + :param object data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str - :raises: TypeError if serialization fails. - :raises: ValueError if data is None + :returns: The serialized URL path + :raises TypeError: if serialization fails. + :raises ValueError: if data is None """ try: output = self.serialize_data(data, data_type, **kwargs) @@ -747,32 +715,30 @@ def url(self, name, data, data_type, **kwargs): if kwargs.get("skip_quote") is True: output = str(output) - # https://github.com/Azure/autorest.python/issues/2063 output = output.replace("{", quote("{")).replace("}", quote("}")) else: output = quote(str(output), safe="") - except SerializationError: - raise TypeError("{} must be type {}.".format(name, data_type)) - else: - return output + except SerializationError as exc: + raise TypeError("{} must be type {}.".format(name, data_type)) from exc + return output def query(self, name, data, data_type, **kwargs): """Serialize data intended for a URL query. - :param data: The data to be serialized. + :param str name: The name of the query parameter. + :param object data: The data to be serialized. :param str data_type: The type to be serialized from. - :keyword bool skip_quote: Whether to skip quote the serialized result. - Defaults to False. - :rtype: str - :raises: TypeError if serialization fails. - :raises: ValueError if data is None + :rtype: str, list + :raises TypeError: if serialization fails. + :raises ValueError: if data is None + :returns: The serialized query parameter """ try: # Treat the list aside, since we don't want to encode the div separator if data_type.startswith("["): internal_data_type = data_type[1:-1] do_quote = not kwargs.get("skip_quote", False) - return str(self.serialize_iter(data, internal_data_type, do_quote=do_quote, **kwargs)) + return self.serialize_iter(data, internal_data_type, do_quote=do_quote, **kwargs) # Not a list, regular serialization output = self.serialize_data(data, data_type, **kwargs) @@ -782,19 +748,20 @@ def query(self, name, data, data_type, **kwargs): output = str(output) else: output = quote(str(output), safe="") - except SerializationError: - raise TypeError("{} must be type {}.".format(name, data_type)) - else: - return str(output) + except SerializationError as exc: + raise TypeError("{} must be type {}.".format(name, data_type)) from exc + return str(output) def header(self, name, data, data_type, **kwargs): """Serialize data intended for a request header. - :param data: The data to be serialized. + :param str name: The name of the header. + :param object data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str - :raises: TypeError if serialization fails. - :raises: ValueError if data is None + :raises TypeError: if serialization fails. + :raises ValueError: if data is None + :returns: The serialized header """ try: if data_type in ["[str]"]: @@ -803,37 +770,36 @@ def header(self, name, data, data_type, **kwargs): output = self.serialize_data(data, data_type, **kwargs) if data_type == "bool": output = json.dumps(output) - except SerializationError: - raise TypeError("{} must be type {}.".format(name, data_type)) - else: - return str(output) + except SerializationError as exc: + raise TypeError("{} must be type {}.".format(name, data_type)) from exc + return str(output) def serialize_data(self, data, data_type, **kwargs): """Serialize generic data according to supplied data type. - :param data: The data to be serialized. + :param object data: The data to be serialized. :param str data_type: The type to be serialized from. - :param bool required: Whether it's essential that the data not be - empty or None - :raises: AttributeError if required data is None. - :raises: ValueError if data is None - :raises: SerializationError if serialization fails. + :raises AttributeError: if required data is None. + :raises ValueError: if data is None + :raises SerializationError: if serialization fails. + :returns: The serialized data. + :rtype: str, int, float, bool, dict, list """ if data is None: raise ValueError("No value for given attribute") try: - if data is AzureCoreNull: + if data is CoreNull: return None if data_type in self.basic_types.values(): return self.serialize_basic(data, data_type, **kwargs) - elif data_type in self.serialize_type: + if data_type in self.serialize_type: return self.serialize_type[data_type](data, **kwargs) # If dependencies is empty, try with current data class # It has to be a subclass of Enum anyway - enum_type = self.dependencies.get(data_type, data.__class__) + enum_type = self.dependencies.get(data_type, cast(type, data.__class__)) if issubclass(enum_type, Enum): return Serializer.serialize_enum(data, enum_obj=enum_type) @@ -843,12 +809,11 @@ def serialize_data(self, data, data_type, **kwargs): except (ValueError, TypeError) as err: msg = "Unable to serialize value: {!r} as type: {!r}." - raise_with_traceback(SerializationError, msg.format(data, data_type), err) - else: - return self._serialize(data, **kwargs) + raise SerializationError(msg.format(data, data_type)) from err + return self._serialize(data, **kwargs) @classmethod - def _get_custom_serializers(cls, data_type, **kwargs): + def _get_custom_serializers(cls, data_type, **kwargs): # pylint: disable=inconsistent-return-statements custom_serializer = kwargs.get("basic_types_serializers", {}).get(data_type) if custom_serializer: return custom_serializer @@ -864,23 +829,33 @@ def serialize_basic(cls, data, data_type, **kwargs): - basic_types_serializers dict[str, callable] : If set, use the callable as serializer - is_xml bool : If set, use xml_basic_types_serializers - :param data: Object to be serialized. + :param obj data: Object to be serialized. :param str data_type: Type of object in the iterable. + :rtype: str, int, float, bool + :return: serialized object + :raises TypeError: raise if data_type is not one of str, int, float, bool. """ custom_serializer = cls._get_custom_serializers(data_type, **kwargs) if custom_serializer: return custom_serializer(data) if data_type == "str": return cls.serialize_unicode(data) - return eval(data_type)(data) # nosec + if data_type == "int": + return int(data) + if data_type == "float": + return float(data) + if data_type == "bool": + return bool(data) + raise TypeError("Unknown basic data type: {}".format(data_type)) @classmethod def serialize_unicode(cls, data): """Special handling for serializing unicode strings in Py2. Encode to UTF-8 if unicode, otherwise handle as a str. - :param data: Object to be serialized. + :param str data: Object to be serialized. :rtype: str + :return: serialized object """ try: # If I received an enum, return its value return data.value @@ -894,8 +869,7 @@ def serialize_unicode(cls, data): return data except NameError: return str(data) - else: - return str(data) + return str(data) def serialize_iter(self, data, iter_type, div=None, **kwargs): """Serialize iterable. @@ -905,15 +879,13 @@ def serialize_iter(self, data, iter_type, div=None, **kwargs): serialization_ctxt['type'] should be same as data_type. - is_xml bool : If set, serialize as XML - :param list attr: Object to be serialized. + :param list data: Object to be serialized. :param str iter_type: Type of object in the iterable. - :param bool required: Whether the objects in the iterable must - not be None or empty. :param str div: If set, this str will be used to combine the elements in the iterable into a combined string. Default is 'None'. - :keyword bool do_quote: Whether to quote the serialized result of each iterable element. Defaults to False. :rtype: list, str + :return: serialized iterable """ if isinstance(data, str): raise SerializationError("Refuse str type as a valid iter type.") @@ -956,11 +928,7 @@ def serialize_iter(self, data, iter_type, div=None, **kwargs): if isinstance(el, ET.Element): el_node = el else: - el_node = _create_xml_node( - node_name, - xml_desc.get("prefix", None), - xml_desc.get("ns", None), - ) + el_node = _create_xml_node(node_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) if el is not None: # Otherwise it writes "None" :-p el_node.text = str(el) final_result.append(el_node) @@ -972,9 +940,8 @@ def serialize_dict(self, attr, dict_type, **kwargs): :param dict attr: Object to be serialized. :param str dict_type: Type of object in the dictionary. - :param bool required: Whether the objects in the dictionary must - not be None or empty. :rtype: dict + :return: serialized dictionary """ serialization_ctxt = kwargs.get("serialization_ctxt", {}) serialized = {} @@ -998,7 +965,7 @@ def serialize_dict(self, attr, dict_type, **kwargs): return serialized - def serialize_object(self, attr, **kwargs): + def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements """Serialize a generic object. This will be handled as a dictionary. If object passed in is not a basic type (str, int, float, dict, list) it will simply be @@ -1006,6 +973,7 @@ def serialize_object(self, attr, **kwargs): :param dict attr: Object to be serialized. :rtype: dict or str + :return: serialized object """ if attr is None: return None @@ -1016,7 +984,7 @@ def serialize_object(self, attr, **kwargs): return self.serialize_basic(attr, self.basic_types[obj_type], **kwargs) if obj_type is _long_type: return self.serialize_long(attr) - if obj_type is unicode_str: + if obj_type is str: return self.serialize_unicode(attr) if obj_type is datetime.datetime: return self.serialize_iso(attr) @@ -1030,7 +998,7 @@ def serialize_object(self, attr, **kwargs): return self.serialize_decimal(attr) # If it's a model or I know this dependency, serialize as a Model - elif obj_type in self.dependencies.values() or isinstance(attr, Model): + if obj_type in self.dependencies.values() or isinstance(attr, Model): return self._serialize(attr) if obj_type == dict: @@ -1061,56 +1029,61 @@ def serialize_enum(attr, enum_obj=None): try: enum_obj(result) # type: ignore return result - except ValueError: + except ValueError as exc: for enum_value in enum_obj: # type: ignore if enum_value.value.lower() == str(attr).lower(): return enum_value.value error = "{!r} is not valid value for enum {!r}" - raise SerializationError(error.format(attr, enum_obj)) + raise SerializationError(error.format(attr, enum_obj)) from exc @staticmethod - def serialize_bytearray(attr, **kwargs): + def serialize_bytearray(attr, **kwargs): # pylint: disable=unused-argument """Serialize bytearray into base-64 string. - :param attr: Object to be serialized. + :param str attr: Object to be serialized. :rtype: str + :return: serialized base64 """ return b64encode(attr).decode() @staticmethod - def serialize_base64(attr, **kwargs): + def serialize_base64(attr, **kwargs): # pylint: disable=unused-argument """Serialize str into base-64 string. - :param attr: Object to be serialized. + :param str attr: Object to be serialized. :rtype: str + :return: serialized base64 """ encoded = b64encode(attr).decode("ascii") return encoded.strip("=").replace("+", "-").replace("/", "_") @staticmethod - def serialize_decimal(attr, **kwargs): + def serialize_decimal(attr, **kwargs): # pylint: disable=unused-argument """Serialize Decimal object to float. - :param attr: Object to be serialized. + :param decimal attr: Object to be serialized. :rtype: float + :return: serialized decimal """ return float(attr) @staticmethod - def serialize_long(attr, **kwargs): + def serialize_long(attr, **kwargs): # pylint: disable=unused-argument """Serialize long (Py2) or int (Py3). - :param attr: Object to be serialized. + :param int attr: Object to be serialized. :rtype: int/long + :return: serialized long """ return _long_type(attr) @staticmethod - def serialize_date(attr, **kwargs): + def serialize_date(attr, **kwargs): # pylint: disable=unused-argument """Serialize Date object into ISO-8601 formatted string. :param Date attr: Object to be serialized. :rtype: str + :return: serialized date """ if isinstance(attr, str): attr = isodate.parse_date(attr) @@ -1118,11 +1091,12 @@ def serialize_date(attr, **kwargs): return t @staticmethod - def serialize_time(attr, **kwargs): + def serialize_time(attr, **kwargs): # pylint: disable=unused-argument """Serialize Time object into ISO-8601 formatted string. :param datetime.time attr: Object to be serialized. :rtype: str + :return: serialized time """ if isinstance(attr, str): attr = isodate.parse_time(attr) @@ -1132,30 +1106,87 @@ def serialize_time(attr, **kwargs): return t @staticmethod - def serialize_duration(attr, **kwargs): + def serialize_duration(attr, **kwargs): # pylint: disable=unused-argument """Serialize TimeDelta object into ISO-8601 formatted string. :param TimeDelta attr: Object to be serialized. :rtype: str + :return: serialized duration """ if isinstance(attr, str): attr = isodate.parse_duration(attr) return isodate.duration_isoformat(attr) @staticmethod - def serialize_rfc(attr, **kwargs): + def _serialize_duration_numeric(attr, scale, as_int): + """Serialize a TimeDelta into a numeric value scaled to the wire unit. + + :param TimeDelta attr: Object to be serialized. + :param int scale: Multiplier applied to total seconds (1 for seconds, 1000 for milliseconds). + :param bool as_int: Whether to truncate the result to an int. + :rtype: int or float + :return: serialized duration + """ + if isinstance(attr, str): + attr = isodate.parse_duration(attr) + value = attr.total_seconds() * scale if isinstance(attr, datetime.timedelta) else attr + return int(value) if as_int else float(value) + + @staticmethod + def serialize_duration_seconds_int(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into an integer number of seconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: int + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1, True) + + @staticmethod + def serialize_duration_seconds_float(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into a floating point number of seconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: float + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1, False) + + @staticmethod + def serialize_duration_milliseconds_int(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into an integer number of milliseconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: int + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1000, True) + + @staticmethod + def serialize_duration_milliseconds_float(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into a floating point number of milliseconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: float + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1000, False) + + @staticmethod + def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument """Serialize Datetime object into RFC-1123 formatted string. :param Datetime attr: Object to be serialized. :rtype: str - :raises: TypeError if format invalid. + :raises TypeError: if format invalid. + :return: serialized rfc """ try: if not attr.tzinfo: _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") utc = attr.utctimetuple() - except AttributeError: - raise TypeError("RFC1123 object must be valid Datetime object.") + except AttributeError as exc: + raise TypeError("RFC1123 object must be valid Datetime object.") from exc return "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT".format( Serializer.days[utc.tm_wday], @@ -1168,12 +1199,13 @@ def serialize_rfc(attr, **kwargs): ) @staticmethod - def serialize_iso(attr, **kwargs): + def serialize_iso(attr, **kwargs): # pylint: disable=unused-argument """Serialize Datetime object into ISO-8601 formatted string. :param Datetime attr: Object to be serialized. :rtype: str - :raises: SerializationError if format invalid. + :raises SerializationError: if format invalid. + :return: serialized iso """ if isinstance(attr, str): attr = isodate.parse_datetime(attr) @@ -1188,29 +1220,25 @@ def serialize_iso(attr, **kwargs): if microseconds: microseconds = "." + microseconds date = "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}".format( - utc.tm_year, - utc.tm_mon, - utc.tm_mday, - utc.tm_hour, - utc.tm_min, - utc.tm_sec, + utc.tm_year, utc.tm_mon, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec ) return date + microseconds + "Z" except (ValueError, OverflowError) as err: msg = "Unable to serialize datetime object." - raise_with_traceback(SerializationError, msg, err) + raise SerializationError(msg) from err except AttributeError as err: msg = "ISO-8601 object must be valid Datetime object." - raise_with_traceback(TypeError, msg, err) + raise TypeError(msg) from err @staticmethod - def serialize_unix(attr, **kwargs): + def serialize_unix(attr, **kwargs): # pylint: disable=unused-argument """Serialize Datetime object into IntTime format. This is represented as seconds. :param Datetime attr: Object to be serialized. :rtype: int - :raises: SerializationError if format invalid + :raises SerializationError: if format invalid + :return: serialied unix """ if isinstance(attr, int): return attr @@ -1218,17 +1246,17 @@ def serialize_unix(attr, **kwargs): if not attr.tzinfo: _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") return int(calendar.timegm(attr.utctimetuple())) - except AttributeError: - raise TypeError("Unix time object must be valid Datetime object.") + except AttributeError as exc: + raise TypeError("Unix time object must be valid Datetime object.") from exc -def rest_key_extractor(attr, attr_desc, data): +def rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument key = attr_desc["key"] working_data = data while "." in key: # Need the cast, as for some reasons "split" is typed as list[str | Any] - dict_keys = cast(List[str], _FLATTEN.split(key)) + dict_keys = cast(list[str], _FLATTEN.split(key)) if len(dict_keys) == 1: key = _decode_attribute_map_key(dict_keys[0]) break @@ -1237,14 +1265,15 @@ def rest_key_extractor(attr, attr_desc, data): if working_data is None: # If at any point while following flatten JSON path see None, it means # that all properties under are None as well - # https://github.com/Azure/msrest-for-python/issues/197 return None key = ".".join(dict_keys[1:]) return working_data.get(key) -def rest_key_case_insensitive_extractor(attr, attr_desc, data): +def rest_key_case_insensitive_extractor( # pylint: disable=unused-argument, inconsistent-return-statements + attr, attr_desc, data +): key = attr_desc["key"] working_data = data @@ -1258,7 +1287,6 @@ def rest_key_case_insensitive_extractor(attr, attr_desc, data): if working_data is None: # If at any point while following flatten JSON path see None, it means # that all properties under are None as well - # https://github.com/Azure/msrest-for-python/issues/197 return None key = ".".join(dict_keys[1:]) @@ -1266,17 +1294,29 @@ def rest_key_case_insensitive_extractor(attr, attr_desc, data): return attribute_key_case_insensitive_extractor(key, None, working_data) -def last_rest_key_extractor(attr, attr_desc, data): - """Extract the attribute in "data" based on the last part of the JSON path key.""" +def last_rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument + """Extract the attribute in "data" based on the last part of the JSON path key. + + :param str attr: The attribute to extract + :param dict attr_desc: The attribute description + :param dict data: The data to extract from + :rtype: object + :returns: The extracted attribute + """ key = attr_desc["key"] dict_keys = _FLATTEN.split(key) return attribute_key_extractor(dict_keys[-1], None, data) -def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): +def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): # pylint: disable=unused-argument """Extract the attribute in "data" based on the last part of the JSON path key. This is the case insensitive version of "last_rest_key_extractor" + :param str attr: The attribute to extract + :param dict attr_desc: The attribute description + :param dict data: The data to extract from + :rtype: object + :returns: The extracted attribute """ key = attr_desc["key"] dict_keys = _FLATTEN.split(key) @@ -1313,7 +1353,7 @@ def _extract_name_from_internal_type(internal_type): return xml_name -def xml_key_extractor(attr, attr_desc, data): +def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument,too-many-return-statements if isinstance(data, dict): return None @@ -1365,22 +1405,21 @@ def xml_key_extractor(attr, attr_desc, data): if is_iter_type: if is_wrapped: return None # is_wrapped no node, we want None - else: - return [] # not wrapped, assume empty list + return [] # not wrapped, assume empty list return None # Assume it's not there, maybe an optional node. # If is_iter_type and not wrapped, return all found children if is_iter_type: if not is_wrapped: return children - else: # Iter and wrapped, should have found one node only (the wrap one) - if len(children) != 1: - raise DeserializationError( - "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( - xml_name - ) + # Iter and wrapped, should have found one node only (the wrap one) + if len(children) != 1: + raise DeserializationError( + "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( + xml_name ) - return list(children[0]) # Might be empty list and that's ok. + ) + return list(children[0]) # Might be empty list and that's ok. # Here it's not a itertype, we should have found one element only or empty if len(children) > 1: @@ -1388,7 +1427,7 @@ def xml_key_extractor(attr, attr_desc, data): return children[0] -class Deserializer(object): +class Deserializer: """Response object model deserializer. :param dict classes: Class type dictionary for deserializing complex types. @@ -1397,14 +1436,18 @@ class Deserializer(object): basic_types = {str: "str", int: "int", bool: "bool", float: "float"} - valid_date = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") + valid_date = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") - def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): + def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: self.deserialize_type = { "iso-8601": Deserializer.deserialize_iso, "rfc-1123": Deserializer.deserialize_rfc, "unix-time": Deserializer.deserialize_unix, "duration": Deserializer.deserialize_duration, + "duration-seconds-int": Deserializer.deserialize_duration_seconds, + "duration-seconds-float": Deserializer.deserialize_duration_seconds, + "duration-milliseconds-int": Deserializer.deserialize_duration_milliseconds, + "duration-milliseconds-float": Deserializer.deserialize_duration_milliseconds, "date": Deserializer.deserialize_date, "time": Deserializer.deserialize_time, "decimal": Deserializer.deserialize_decimal, @@ -1417,9 +1460,13 @@ def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): } self.deserialize_expected_types = { "duration": (isodate.Duration, datetime.timedelta), + "duration-seconds-int": (isodate.Duration, datetime.timedelta), + "duration-seconds-float": (isodate.Duration, datetime.timedelta), + "duration-milliseconds-int": (isodate.Duration, datetime.timedelta), + "duration-milliseconds-float": (isodate.Duration, datetime.timedelta), "iso-8601": (datetime.datetime), } - self.dependencies: Dict[str, Type[ModelType]] = dict(classes) if classes else {} + self.dependencies: dict[str, type] = dict(classes) if classes else {} self.key_extractors = [rest_key_extractor, xml_key_extractor] # Additional properties only works if the "rest_key_extractor" is used to # extract the keys. Making it to work whatever the key extractor is too much @@ -1429,33 +1476,56 @@ def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): # Otherwise, result are unexpected self.additional_properties_detection = True - def __call__(self, target_obj, response_data, content_type=None): + def __call__(self, target_obj, response_data, content_type=None): # pylint: disable=too-many-return-statements """Call the deserializer to process a REST response. :param str target_obj: Target data type to deserialize to. :param requests.Response response_data: REST response object. :param str content_type: Swagger "produces" if available. - :raises: DeserializationError if deserialization fails. + :raises DeserializationError: if deserialization fails. :return: Deserialized object. + :rtype: object """ + # Fast path for header deserialization: response_data is a plain str or None + # and target_obj is a simple scalar type. This avoids the expensive + # _unpack_content → _deserialize → _classify_target → deserialize_data chain. + if response_data is None: + return None + if target_obj == "str" and isinstance(response_data, str): + return response_data + if isinstance(response_data, str): + if target_obj == "int": + return int(response_data) + if target_obj == "bool": + if response_data in ("true", "1", "True"): + return True + if response_data in ("false", "0", "False"): + return False + return bool(response_data) + if target_obj == "rfc-1123": + return Deserializer.deserialize_rfc(response_data) + if target_obj == "bytearray": + return Deserializer.deserialize_bytearray(response_data) + data = self._unpack_content(response_data, content_type) return self._deserialize(target_obj, data) - def _deserialize(self, target_obj, data): + def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return-statements """Call the deserializer on a model. Data needs to be already deserialized as JSON or XML ElementTree :param str target_obj: Target data type to deserialize to. :param object data: Object to deserialize. - :raises: DeserializationError if deserialization fails. + :raises DeserializationError: if deserialization fails. :return: Deserialized object. + :rtype: object """ # This is already a model, go recursive just in case if hasattr(data, "_attribute_map"): constants = [name for name, config in getattr(data, "_validation", {}).items() if config.get("constant")] try: - for attr, mapconfig in data._attribute_map.items(): + for attr, mapconfig in data._attribute_map.items(): # pylint: disable=protected-access if attr in constants: continue value = getattr(data, attr) @@ -1472,15 +1542,15 @@ def _deserialize(self, target_obj, data): response, class_name = self._classify_target(target_obj, data) - if isinstance(response, basestring): + if isinstance(response, str): return self.deserialize_data(data, response) - elif isinstance(response, type) and issubclass(response, Enum): + if isinstance(response, type) and issubclass(response, Enum): return self.deserialize_enum(data, response) - if data is None: + if data is None or data is CoreNull: return data try: - attributes = response._attribute_map # type: ignore + attributes = response._attribute_map # type: ignore # pylint: disable=protected-access d_attrs = {} for attr, attr_desc in attributes.items(): # Check empty string. If it's not empty, someone has a real "additionalProperties"... @@ -1509,10 +1579,9 @@ def _deserialize(self, target_obj, data): d_attrs[attr] = value except (AttributeError, TypeError, KeyError) as err: msg = "Unable to deserialize to object: " + class_name # type: ignore - raise_with_traceback(DeserializationError, msg, err) - else: - additional_properties = self._build_additional_properties(attributes, data) - return self._instantiate_model(response, d_attrs, additional_properties) + raise DeserializationError(msg) from err + additional_properties = self._build_additional_properties(attributes, data) + return self._instantiate_model(response, d_attrs, additional_properties) def _build_additional_properties(self, attribute_map, data): if not self.additional_properties_detection: @@ -1539,18 +1608,20 @@ def _classify_target(self, target, data): :param str target: The target object type to deserialize to. :param str/dict data: The response data to deserialize. + :return: The classified target object and its class name. + :rtype: tuple """ if target is None: return None, None - if isinstance(target, basestring): + if isinstance(target, str): try: target = self.dependencies[target] except KeyError: return target, target try: - target = target._classify(data, self.dependencies) + target = target._classify(data, self.dependencies) # type: ignore # pylint: disable=protected-access except AttributeError: pass # Target is not a Model, no classify return target, target.__class__.__name__ # type: ignore @@ -1565,13 +1636,14 @@ def failsafe_deserialize(self, target_obj, data, content_type=None): :param str target_obj: The target object type to deserialize to. :param str/dict data: The response data to deserialize. :param str content_type: Swagger "produces" if available. + :return: Deserialized object. + :rtype: object """ try: return self(target_obj, data, content_type=content_type) - except: + except: # pylint: disable=bare-except _LOGGER.debug( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", - exc_info=True, + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True ) return None @@ -1587,10 +1659,12 @@ def _unpack_content(raw_data, content_type=None): If raw_data is something else, bypass all logic and return it directly. - :param raw_data: Data to be processed. - :param content_type: How to parse if raw_data is a string/bytes. + :param obj raw_data: Data to be processed. + :param str content_type: How to parse if raw_data is a string/bytes. :raises JSONDecodeError: If JSON is requested and parsing is impossible. :raises UnicodeDecodeError: If bytes is not UTF8 + :rtype: object + :return: Unpacked content. """ # Assume this is enough to detect a Pipeline Response without importing it context = getattr(raw_data, "context", {}) @@ -1607,31 +1681,42 @@ def _unpack_content(raw_data, content_type=None): if hasattr(raw_data, "_content_consumed"): return RawDeserializer.deserialize_from_http_generics(raw_data.text, raw_data.headers) - if isinstance(raw_data, (basestring, bytes)) or hasattr(raw_data, "read"): + if isinstance(raw_data, (str, bytes)) or hasattr(raw_data, "read"): return RawDeserializer.deserialize_from_text(raw_data, content_type) # type: ignore return raw_data def _instantiate_model(self, response, attrs, additional_properties=None): """Instantiate a response model passing in deserialized args. - :param response: The response model class. - :param d_attrs: The deserialized response attributes. + :param Response response: The response model class. + :param dict attrs: The deserialized response attributes. + :param dict additional_properties: Additional properties to be set. + :rtype: Response + :return: The instantiated response model. """ if callable(response): subtype = getattr(response, "_subtype_map", {}) try: - readonly = [k for k, v in response._validation.items() if v.get("readonly")] - const = [k for k, v in response._validation.items() if v.get("constant")] + readonly = [ + k + for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore + if v.get("readonly") + ] + const = [ + k + for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore + if v.get("constant") + ] kwargs = {k: v for k, v in attrs.items() if k not in subtype and k not in readonly + const} response_obj = response(**kwargs) for attr in readonly: setattr(response_obj, attr, attrs.get(attr)) if additional_properties: - response_obj.additional_properties = additional_properties + response_obj.additional_properties = additional_properties # type: ignore return response_obj except TypeError as err: msg = "Unable to deserialize {} into model {}. ".format(kwargs, response) # type: ignore - raise DeserializationError(msg + str(err)) + raise DeserializationError(msg + str(err)) from err else: try: for attr, value in attrs.items(): @@ -1640,15 +1725,16 @@ def _instantiate_model(self, response, attrs, additional_properties=None): except Exception as exp: msg = "Unable to populate response model. " msg += "Type: {}, Error: {}".format(type(response), exp) - raise DeserializationError(msg) + raise DeserializationError(msg) from exp - def deserialize_data(self, data, data_type): + def deserialize_data(self, data, data_type): # pylint: disable=too-many-return-statements """Process data for deserialization according to data type. :param str data: The response string to be deserialized. :param str data_type: The type to deserialize to. - :raises: DeserializationError if deserialization fails. + :raises DeserializationError: if deserialization fails. :return: Deserialized object. + :rtype: object """ if data is None: return data @@ -1662,7 +1748,11 @@ def deserialize_data(self, data, data_type): if isinstance(data, self.deserialize_expected_types.get(data_type, tuple())): return data - is_a_text_parsing_type = lambda x: x not in ["object", "[]", r"{}"] + is_a_text_parsing_type = lambda x: x not in [ # pylint: disable=unnecessary-lambda-assignment + "object", + "[]", + r"{}", + ] if isinstance(data, ET.Element) and is_a_text_parsing_type(data_type) and not data.text: return None data_val = self.deserialize_type[data_type](data) @@ -1681,15 +1771,15 @@ def deserialize_data(self, data, data_type): except (ValueError, TypeError, AttributeError) as err: msg = "Unable to deserialize response data." msg += " Data: {}, {}".format(data, data_type) - raise_with_traceback(DeserializationError, msg, err) - else: - return self._deserialize(obj_type, data) + raise DeserializationError(msg) from err + return self._deserialize(obj_type, data) def deserialize_iter(self, attr, iter_type): """Deserialize an iterable. :param list attr: Iterable to be deserialized. :param str iter_type: The type of object in the iterable. + :return: Deserialized iterable. :rtype: list """ if attr is None: @@ -1706,6 +1796,7 @@ def deserialize_dict(self, attr, dict_type): :param dict/list attr: Dictionary to be deserialized. Also accepts a list of key, value pairs. :param str dict_type: The object type of the items in the dictionary. + :return: Deserialized dictionary. :rtype: dict """ if isinstance(attr, list): @@ -1716,20 +1807,21 @@ def deserialize_dict(self, attr, dict_type): attr = {el.tag: el.text for el in attr} return {k: self.deserialize_data(v, dict_type) for k, v in attr.items()} - def deserialize_object(self, attr, **kwargs): + def deserialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements """Deserialize a generic object. This will be handled as a dictionary. :param dict attr: Dictionary to be deserialized. + :return: Deserialized object. :rtype: dict - :raises: TypeError if non-builtin datatype encountered. + :raises TypeError: if non-builtin datatype encountered. """ if attr is None: return None if isinstance(attr, ET.Element): # Do no recurse on XML, just return the tree as-is return attr - if isinstance(attr, basestring): + if isinstance(attr, str): return self.deserialize_basic(attr, "str") obj_type = type(attr) if obj_type in self.basic_types: @@ -1755,11 +1847,10 @@ def deserialize_object(self, attr, **kwargs): pass return deserialized - else: - error = "Cannot deserialize generic object with type: " - raise TypeError(error + str(obj_type)) + error = "Cannot deserialize generic object with type: " + raise TypeError(error + str(obj_type)) - def deserialize_basic(self, attr, data_type): + def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return-statements """Deserialize basic builtin data type from string. Will attempt to convert to str, int, float and bool. This function will also accept '1', '0', 'true' and 'false' as @@ -1767,8 +1858,9 @@ def deserialize_basic(self, attr, data_type): :param str attr: response string to be deserialized. :param str data_type: deserialization data type. + :return: Deserialized basic type. :rtype: str, int, float or bool - :raises: TypeError if string format is not valid. + :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool. """ # If we're here, data is supposed to be a basic type. # If it's still an XML node, take the text @@ -1778,24 +1870,27 @@ def deserialize_basic(self, attr, data_type): if data_type == "str": # None or '', node is empty string. return "" - else: - # None or '', node with a strong type is None. - # Don't try to model "empty bool" or "empty int" - return None + # None or '', node with a strong type is None. + # Don't try to model "empty bool" or "empty int" + return None if data_type == "bool": if attr in [True, False, 1, 0]: return bool(attr) - elif isinstance(attr, basestring): + if isinstance(attr, str): if attr.lower() in ["true", "1"]: return True - elif attr.lower() in ["false", "0"]: + if attr.lower() in ["false", "0"]: return False raise TypeError("Invalid boolean value: {}".format(attr)) if data_type == "str": return self.deserialize_unicode(attr) - return eval(data_type)(attr) # nosec + if data_type == "int": + return int(attr) + if data_type == "float": + return float(attr) + raise TypeError("Unknown basic data type: {}".format(data_type)) @staticmethod def deserialize_unicode(data): @@ -1803,6 +1898,7 @@ def deserialize_unicode(data): as a string. :param str data: response string to be deserialized. + :return: Deserialized string. :rtype: str or unicode """ # We might be here because we have an enum modeled as string, @@ -1816,8 +1912,7 @@ def deserialize_unicode(data): return data except NameError: return str(data) - else: - return str(data) + return str(data) @staticmethod def deserialize_enum(data, enum_obj): @@ -1829,6 +1924,7 @@ def deserialize_enum(data, enum_obj): :param str data: Response string to be deserialized. If this value is None or invalid it will be returned as-is. :param Enum enum_obj: Enum object to deserialize to. + :return: Deserialized enum object. :rtype: Enum """ if isinstance(data, enum_obj) or data is None: @@ -1837,12 +1933,11 @@ def deserialize_enum(data, enum_obj): data = data.value if isinstance(data, int): # Workaround. We might consider remove it in the future. - # https://github.com/Azure/azure-rest-api-specs/issues/141 try: return list(enum_obj.__members__.values())[data] - except IndexError: + except IndexError as exc: error = "{!r} is not a valid index for enum {!r}" - raise DeserializationError(error.format(data, enum_obj)) + raise DeserializationError(error.format(data, enum_obj)) from exc try: return enum_obj(str(data)) except ValueError: @@ -1850,11 +1945,7 @@ def deserialize_enum(data, enum_obj): if enum_value.value.lower() == str(data).lower(): return enum_value # We don't fail anymore for unknown value, we deserialize as a string - _LOGGER.warning( - "Deserializer is not able to find %s as valid enum in %s", - data, - enum_obj, - ) + _LOGGER.warning("Deserializer is not able to find %s as valid enum in %s", data, enum_obj) return Deserializer.deserialize_unicode(data) @staticmethod @@ -1862,8 +1953,9 @@ def deserialize_bytearray(attr): """Deserialize string into bytearray. :param str attr: response string to be deserialized. + :return: Deserialized bytearray :rtype: bytearray - :raises: TypeError if string format invalid. + :raises TypeError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1874,8 +1966,9 @@ def deserialize_base64(attr): """Deserialize base64 encoded string into string. :param str attr: response string to be deserialized. + :return: Deserialized base64 string :rtype: bytearray - :raises: TypeError if string format invalid. + :raises TypeError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1889,24 +1982,26 @@ def deserialize_decimal(attr): """Deserialize string into Decimal object. :param str attr: response string to be deserialized. - :rtype: Decimal - :raises: DeserializationError if string format invalid. + :return: Deserialized decimal + :raises DeserializationError: if string format invalid. + :rtype: decimal """ if isinstance(attr, ET.Element): attr = attr.text try: - return decimal.Decimal(attr) # type: ignore + return decimal.Decimal(str(attr)) # type: ignore except decimal.DecimalException as err: msg = "Invalid decimal {}".format(attr) - raise_with_traceback(DeserializationError, msg, err) + raise DeserializationError(msg) from err @staticmethod def deserialize_long(attr): """Deserialize string into long (Py2) or int (Py3). :param str attr: response string to be deserialized. + :return: Deserialized int :rtype: long or int - :raises: ValueError if string format invalid. + :raises ValueError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1917,8 +2012,9 @@ def deserialize_duration(attr): """Deserialize ISO-8601 formatted string into TimeDelta object. :param str attr: response string to be deserialized. + :return: Deserialized duration :rtype: TimeDelta - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1926,17 +2022,59 @@ def deserialize_duration(attr): duration = isodate.parse_duration(attr) except (ValueError, OverflowError, AttributeError) as err: msg = "Cannot deserialize duration object." - raise_with_traceback(DeserializationError, msg, err) - else: - return duration + raise DeserializationError(msg) from err + return duration + + @staticmethod + def _deserialize_duration_numeric(attr, unit): + """Deserialize a numeric duration value into a TimeDelta object. + + :param float attr: response value to be deserialized. + :param str unit: The wire unit, used as the ``timedelta`` keyword + (``"seconds"`` or ``"milliseconds"``). + :return: Deserialized duration + :rtype: TimeDelta + :raises DeserializationError: if value is invalid. + """ + if isinstance(attr, ET.Element): + attr = attr.text + try: + duration = datetime.timedelta(**{unit: float(attr)}) # type: ignore + except (ValueError, OverflowError, TypeError) as err: + msg = "Cannot deserialize duration object." + raise DeserializationError(msg) from err + return duration + + @staticmethod + def deserialize_duration_seconds(attr): + """Deserialize a numeric number of seconds into a TimeDelta object. + + :param float attr: response value to be deserialized. + :return: Deserialized duration + :rtype: TimeDelta + :raises DeserializationError: if value is invalid. + """ + return Deserializer._deserialize_duration_numeric(attr, "seconds") + + @staticmethod + def deserialize_duration_milliseconds(attr): + """Deserialize a numeric number of milliseconds into a TimeDelta object. + + :param float attr: response value to be deserialized. + :return: Deserialized duration + :rtype: TimeDelta + :raises DeserializationError: if value is invalid. + """ + return Deserializer._deserialize_duration_numeric(attr, "milliseconds") @staticmethod def deserialize_date(attr): """Deserialize ISO-8601 formatted string into Date object. :param str attr: response string to be deserialized. + :return: Deserialized date :rtype: Date - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1950,8 +2088,9 @@ def deserialize_time(attr): """Deserialize ISO-8601 formatted string into time object. :param str attr: response string to be deserialized. + :return: Deserialized time :rtype: datetime.time - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1964,31 +2103,32 @@ def deserialize_rfc(attr): """Deserialize RFC-1123 formatted string into Datetime object. :param str attr: response string to be deserialized. + :return: Deserialized RFC datetime :rtype: Datetime - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text try: parsed_date = email.utils.parsedate_tz(attr) # type: ignore date_obj = datetime.datetime( - *parsed_date[:6], tzinfo=_FixedOffset(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) + *parsed_date[:6], tzinfo=datetime.timezone(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) ) if not date_obj.tzinfo: date_obj = date_obj.astimezone(tz=TZ_UTC) except ValueError as err: msg = "Cannot deserialize to rfc datetime object." - raise_with_traceback(DeserializationError, msg, err) - else: - return date_obj + raise DeserializationError(msg) from err + return date_obj @staticmethod def deserialize_iso(attr): """Deserialize ISO-8601 formatted string into Datetime object. :param str attr: response string to be deserialized. + :return: Deserialized ISO datetime :rtype: Datetime - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2015,9 +2155,8 @@ def deserialize_iso(attr): raise OverflowError("Hit max or min date") except (ValueError, OverflowError, AttributeError) as err: msg = "Cannot deserialize datetime object." - raise_with_traceback(DeserializationError, msg, err) - else: - return date_obj + raise DeserializationError(msg) from err + return date_obj @staticmethod def deserialize_unix(attr): @@ -2025,15 +2164,16 @@ def deserialize_unix(attr): This is represented as seconds. :param int attr: Object to be serialized. + :return: Deserialized datetime :rtype: Datetime - :raises: DeserializationError if format invalid + :raises DeserializationError: if format invalid """ if isinstance(attr, ET.Element): attr = int(attr.text) # type: ignore try: + attr = int(attr) date_obj = datetime.datetime.fromtimestamp(attr, TZ_UTC) except ValueError as err: msg = "Cannot deserialize to unix datetime object." - raise_with_traceback(DeserializationError, msg, err) - else: - return date_obj + raise DeserializationError(msg) from err + return date_obj diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py similarity index 54% rename from sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py rename to sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py index 2069daa52a02..4e2942f574c4 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py @@ -1,9 +1,9 @@ -# ------------------------------------------------------------------------- +# coding=utf-8 +# -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from ._communication_identity_client_async import CommunicationIdentityClient - -__all__ = ["CommunicationIdentityClient"] +VERSION = "1.6.0" diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py index 7d62ac6effd1..1ddad049d3ea 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py @@ -2,22 +2,28 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +# pylint: disable=wrong-import-position -from ._client import CommunicationIdentityClient +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._patch import * # pylint: disable=unused-wildcard-import + +from ._client import IdentityClient # type: ignore try: from ._patch import __all__ as _patch_all - from ._patch import * # pylint: disable=unused-wildcard-import + from ._patch import * except ImportError: _patch_all = [] from ._patch import patch_sdk as _patch_sdk __all__ = [ - "CommunicationIdentityClient", + "IdentityClient", ] -__all__.extend([p for p in _patch_all if p not in __all__]) +__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py index 0c09e59bf350..2aaabe8b1832 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py @@ -2,33 +2,47 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, Awaitable from azure.core import AsyncPipelineClient +from azure.core.pipeline import policies from azure.core.rest import AsyncHttpResponse, HttpRequest -from .. import models as _models -from .._serialization import Deserializer, Serializer -from ._configuration import CommunicationIdentityClientConfiguration -from .operations import CommunicationIdentityOperations +from .._utils.serialization import Deserializer, Serializer +from ._configuration import IdentityClientConfiguration +from .operations import IdentityOperationsOperations, TeamsExtensionOperationsOperations, TeamsUserOperationsOperations +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore -class CommunicationIdentityClient: # pylint: disable=client-accepts-api-version-keyword + +class IdentityClient: # pylint: disable=docstring-keyword-should-match-keyword-only """Azure Communication Identity Service. - :ivar communication_identity: CommunicationIdentityOperations operations - :vartype communication_identity: - azure.communication.identity.aio.operations.CommunicationIdentityOperations + :ivar identity_operations: IdentityOperationsOperations operations + :vartype identity_operations: + azure.communication.identity.aio.operations.IdentityOperationsOperations + :ivar teams_user_operations: TeamsUserOperationsOperations operations + :vartype teams_user_operations: + azure.communication.identity.aio.operations.TeamsUserOperationsOperations + :ivar teams_extension_operations: TeamsExtensionOperationsOperations operations + :vartype teams_extension_operations: + azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations :param endpoint: The communication resource, for example - https://my-resource.communication.azure.com. Required. + `https://my-resource.communication.azure.com `_. + Required. :type endpoint: str - :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" + and None. Default value is None. If not set, the operation's default API version will be used. + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ @@ -36,18 +50,43 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self, endpoint: str, **kwargs: Any ) -> None: _endpoint = "{endpoint}" - self._config = CommunicationIdentityClientConfiguration(endpoint=endpoint, **kwargs) - self._client: AsyncPipelineClient = AsyncPipelineClient(base_url=_endpoint, config=self._config, **kwargs) - - client_models = {k: v for k, v in _models.__dict__.items() if isinstance(v, type)} - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) + self._config = IdentityClientConfiguration(endpoint=endpoint, **kwargs) + + _policies = kwargs.pop("policies", None) + if _policies is None: + _policies = [ + policies.RequestIdPolicy(**kwargs), + self._config.headers_policy, + self._config.user_agent_policy, + self._config.proxy_policy, + policies.ContentDecodePolicy(**kwargs), + self._config.redirect_policy, + self._config.retry_policy, + self._config.authentication_policy, + self._config.custom_hook_policy, + self._config.logging_policy, + policies.DistributedTracingPolicy(**kwargs), + policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None, + self._config.http_logging_policy, + ] + self._client: AsyncPipelineClient = AsyncPipelineClient(base_url=_endpoint, policies=_policies, **kwargs) + + self._serialize = Serializer() + self._deserialize = Deserializer() self._serialize.client_side_validation = False - self.communication_identity = CommunicationIdentityOperations( + self.identity_operations = IdentityOperationsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.teams_user_operations = TeamsUserOperationsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.teams_extension_operations = TeamsExtensionOperationsOperations( self._client, self._config, self._serialize, self._deserialize ) - def send_request(self, request: HttpRequest, **kwargs: Any) -> Awaitable[AsyncHttpResponse]: + def send_request( + self, request: HttpRequest, *, stream: bool = False, **kwargs: Any + ) -> Awaitable[AsyncHttpResponse]: """Runs the network request through the client's chained policies. >>> from azure.core.rest import HttpRequest @@ -71,12 +110,12 @@ def send_request(self, request: HttpRequest, **kwargs: Any) -> Awaitable[AsyncHt } request_copy.url = self._client.format_url(request_copy.url, **path_format_arguments) - return self._client.send_request(request_copy, **kwargs) + return self._client.send_request(request_copy, stream=stream, **kwargs) # type: ignore async def close(self) -> None: await self._client.close() - async def __aenter__(self) -> "CommunicationIdentityClient": + async def __aenter__(self) -> Self: await self._client.__aenter__() return self diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py index fe759886cbce..d67c45d46467 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py @@ -1,45 +1,45 @@ +# pylint: disable=line-too-long,useless-suppression # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from typing import Any -from azure.core.configuration import Configuration from azure.core.pipeline import policies -VERSION = "unknown" +from .._version import VERSION -class CommunicationIdentityClientConfiguration( # pylint: disable=too-many-instance-attributes,name-too-long - Configuration -): - """Configuration for CommunicationIdentityClient. +class IdentityClientConfiguration: # pylint: disable=too-many-instance-attributes,docstring-keyword-should-match-keyword-only + """Configuration for IdentityClient. Note that all parameters used to create this instance are saved as instance attributes. :param endpoint: The communication resource, for example - https://my-resource.communication.azure.com. Required. + `https://my-resource.communication.azure.com `_. + Required. :type endpoint: str - :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" + and None. Default value is None. If not set, the operation's default API version will be used. + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, **kwargs: Any) -> None: - super(CommunicationIdentityClientConfiguration, self).__init__(**kwargs) - api_version: str = kwargs.pop("api_version", "2023-10-01") + api_version: str = kwargs.pop("api_version", "2026-09-23") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") self.endpoint = endpoint self.api_version = api_version - kwargs.setdefault("sdk_moniker", "communicationidentityclient/{}".format(VERSION)) + kwargs.setdefault("sdk_moniker", "communication-identity/{}".format(VERSION)) + self.polling_interval = kwargs.get("polling_interval", 30) self._configure(**kwargs) def _configure(self, **kwargs: Any) -> None: @@ -48,7 +48,7 @@ def _configure(self, **kwargs: Any) -> None: self.proxy_policy = kwargs.get("proxy_policy") or policies.ProxyPolicy(**kwargs) self.logging_policy = kwargs.get("logging_policy") or policies.NetworkTraceLoggingPolicy(**kwargs) self.http_logging_policy = kwargs.get("http_logging_policy") or policies.HttpLoggingPolicy(**kwargs) - self.retry_policy = kwargs.get("retry_policy") or policies.AsyncRetryPolicy(**kwargs) self.custom_hook_policy = kwargs.get("custom_hook_policy") or policies.CustomHookPolicy(**kwargs) self.redirect_policy = kwargs.get("redirect_policy") or policies.AsyncRedirectPolicy(**kwargs) + self.retry_policy = kwargs.get("retry_policy") or policies.AsyncRetryPolicy(**kwargs) self.authentication_policy = kwargs.get("authentication_policy") diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py index 17dbc073e01b..ea765788358a 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py @@ -1,32 +1,20 @@ # coding=utf-8 # -------------------------------------------------------------------------- -# # Copyright (c) Microsoft Corporation. All rights reserved. -# -# The MIT License (MIT) -# -# 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. -# +# Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------- +"""Customize generated code here. + +Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize +""" + +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level -# This file is used for handwritten extensions to the generated code. Example: -# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/customize_code/how-to-patch-sdk-code.md def patch_sdk(): - pass + """Do not remove from this file. + + `patch_sdk` is a last resort escape hatch that allows you to do customizations + you can't accomplish using the techniques described in + https://aka.ms/azsdk/python/dpcodegen/python/customize + """ diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py index 2c8ddc0b3dd9..46690414aedf 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py @@ -2,18 +2,28 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +# pylint: disable=wrong-import-position -from ._operations import CommunicationIdentityOperations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._patch import * # pylint: disable=unused-wildcard-import + +from ._operations import IdentityOperationsOperations # type: ignore +from ._operations import TeamsUserOperationsOperations # type: ignore +from ._operations import TeamsExtensionOperationsOperations # type: ignore from ._patch import __all__ as _patch_all -from ._patch import * # pylint: disable=unused-wildcard-import +from ._patch import * from ._patch import patch_sdk as _patch_sdk __all__ = [ - "CommunicationIdentityOperations", + "IdentityOperationsOperations", + "TeamsUserOperationsOperations", + "TeamsExtensionOperationsOperations", ] -__all__.extend([p for p in _patch_all if p not in __all__]) +__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py index d3449860771f..1db7bd9ffd84 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py @@ -3,18 +3,23 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from collections.abc import MutableMapping from io import IOBase -from typing import Any, Callable, Dict, IO, Optional, TypeVar, Union, overload +import json +from typing import Any, Callable, IO, Optional, TypeVar, Union, overload +from azure.core import AsyncPipelineClient from azure.core.exceptions import ( ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, ResourceNotModifiedError, + StreamClosedError, + StreamConsumedError, map_error, ) from azure.core.pipeline import PipelineResponse @@ -22,37 +27,42 @@ from azure.core.tracing.decorator_async import distributed_trace_async from azure.core.utils import case_insensitive_dict -from ... import models as _models +from ... import models as _models, types as _types +from ..._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize +from ..._utils.serialization import Deserializer, Serializer from ...operations._operations import ( - build_communication_identity_create_request, - build_communication_identity_delete_request, - build_communication_identity_exchange_teams_user_access_token_request, - build_communication_identity_issue_access_token_request, - build_communication_identity_revoke_access_tokens_request, + build_identity_operations_create_request, + build_identity_operations_delete_request, + build_identity_operations_issue_access_token_request, + build_identity_operations_revoke_access_tokens_request, + build_teams_extension_operations_delete_assignment_request, + build_teams_extension_operations_exchange_token_request, + build_teams_extension_operations_get_assignment_request, + build_teams_extension_operations_upsert_assignment_request, + build_teams_user_operations_exchange_teams_user_access_token_request, ) +from .._configuration import IdentityClientConfiguration T = TypeVar("T") -ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, dict[str, Any]], Any]] -class CommunicationIdentityOperations: +class IdentityOperationsOperations: # pylint: disable=docstring-missing-param """ .. warning:: **DO NOT** instantiate this class directly. Instead, you should access the following operations through - :class:`~azure.communication.identity.aio.CommunicationIdentityClient`'s - :attr:`communication_identity` attribute. + :class:`~azure.communication.identity.aio.IdentityClient`'s + :attr:`identity_operations` attribute. """ - models = _models - def __init__(self, *args, **kwargs) -> None: input_args = list(args) - self._client = input_args.pop(0) if input_args else kwargs.pop("client") - self._config = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize = input_args.pop(0) if input_args else kwargs.pop("deserializer") + self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload async def create( @@ -68,18 +78,23 @@ async def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest + :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @overload async def create( - self, body: Optional[IO] = None, *, content_type: str = "application/json", **kwargs: Any + self, + body: Optional[_types.CommunicationIdentityCreateRequest] = None, + *, + content_type: str = "application/json", + **kwargs: Any ) -> _models.CommunicationIdentityAccessTokenResult: """Create a new identity, and optionally, an access token. @@ -87,18 +102,43 @@ async def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: IO + :type body: ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def create( + self, body: Optional[IO[bytes]] = None, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Create a new identity, and optionally, an access token. + + Create a new identity, and optionally, an access token. + + :param body: If specified, creates also a Communication Identity access token associated with + the identity and containing the requested scopes. Default value is None. + :type body: IO[bytes] :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace_async async def create( - self, body: Optional[Union[_models.CommunicationIdentityCreateRequest, IO]] = None, **kwargs: Any + self, + body: Optional[ + Union[_models.CommunicationIdentityCreateRequest, _types.CommunicationIdentityCreateRequest, IO[bytes]] + ] = None, + **kwargs: Any ) -> _models.CommunicationIdentityAccessTokenResult: """Create a new identity, and optionally, an access token. @@ -106,16 +146,16 @@ async def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Is either a - CommunicationIdentityCreateRequest type or a IO type. Default value is None. - :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest or IO - :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. - Default value is None. - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult + CommunicationIdentityCreateRequest type or a IO[bytes] type. Default value is None. + :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest + or ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest or + IO[bytes] + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -127,23 +167,22 @@ async def create( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + content_type = content_type if body else None cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) - content_type = content_type or "application/json" - _json = None + content_type = content_type or "application/json" if body else None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: if body is not None: - _json = self._serialize.body(body, "CommunicationIdentityCreateRequest") + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore else: - _json = None + _content = None - request = build_communication_identity_create_request( + _request = build_identity_operations_create_request( content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, @@ -151,31 +190,41 @@ async def create( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [201]: if _stream: - await response.read() # Load the body in memory and close the socket + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) - deserialized = self._deserialize("CommunicationIdentityAccessTokenResult", pipeline_response) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) if cls: - return cls(pipeline_response, deserialized, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore - return deserialized + return deserialized # type: ignore @distributed_trace_async - async def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements + async def delete(self, id: str, **kwargs: Any) -> None: """Delete the identity, revoke all tokens for the identity and delete all associated data. Delete the identity, revoke all tokens for the identity and delete all associated data. @@ -186,7 +235,7 @@ async def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=incon :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -199,7 +248,7 @@ async def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=incon cls: ClsType[None] = kwargs.pop("cls", None) - request = build_communication_identity_delete_request( + _request = build_identity_operations_delete_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -208,29 +257,28 @@ async def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=incon path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: - if _stream: - await response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) if cls: - return cls(pipeline_response, None, {}) + return cls(pipeline_response, None, {}) # type: ignore @distributed_trace_async - async def revoke_access_tokens( # pylint: disable=inconsistent-return-statements - self, id: str, **kwargs: Any - ) -> None: + async def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: """Revoke all access tokens for the specific identity. Revoke all access tokens for the specific identity. @@ -241,7 +289,7 @@ async def revoke_access_tokens( # pylint: disable=inconsistent-return-statement :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -254,7 +302,7 @@ async def revoke_access_tokens( # pylint: disable=inconsistent-return-statement cls: ClsType[None] = kwargs.pop("cls", None) - request = build_communication_identity_revoke_access_tokens_request( + _request = build_identity_operations_revoke_access_tokens_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -263,86 +311,295 @@ async def revoke_access_tokens( # pylint: disable=inconsistent-return-statement path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore + + @overload + async def issue_access_token( + self, + id: str, + body: _models.CommunicationIdentityAccessTokenRequest, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: + ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def issue_access_token( + self, + id: str, + body: _types.CommunicationIdentityAccessTokenRequest, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: + ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def issue_access_token( + self, id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace_async + async def issue_access_token( + self, + id: str, + body: Union[ + _models.CommunicationIdentityAccessTokenRequest, _types.CommunicationIdentityAccessTokenRequest, IO[bytes] + ], + **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Is either a + CommunicationIdentityAccessTokenRequest type or a IO[bytes] type. Required. + :type body: + ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest or + ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest or + IO[bytes] + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_identity_operations_issue_access_token_request( + id=id, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: if _stream: - await response.read() # Load the body in memory and close the socket + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) + if cls: - return cls(pipeline_response, None, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + +class TeamsUserOperationsOperations: # pylint: disable=docstring-missing-param + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~azure.communication.identity.aio.IdentityClient`'s + :attr:`teams_user_operations` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload async def exchange_teams_user_access_token( self, body: _models.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. + + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. + + :param body: Request payload for the token exchange. Required. + :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def exchange_teams_user_access_token( + self, body: _types.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. - Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest + :type body: ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @overload async def exchange_teams_user_access_token( - self, body: IO, *, content_type: str = "application/json", **kwargs: Any + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. - Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: IO + :type body: IO[bytes] :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace_async async def exchange_teams_user_access_token( - self, body: Union[_models.TeamsUserExchangeTokenRequest, IO], **kwargs: Any + self, + body: Union[_models.TeamsUserExchangeTokenRequest, _types.TeamsUserExchangeTokenRequest, IO[bytes]], + **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. - Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. :param body: Request payload for the token exchange. Is either a TeamsUserExchangeTokenRequest - type or a IO type. Required. - :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest or IO - :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. - Default value is None. - :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + type or a IO[bytes] type. Required. + :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest or + ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest or IO[bytes] + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -357,17 +614,15 @@ async def exchange_teams_user_access_token( cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) content_type = content_type or "application/json" - _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "TeamsUserExchangeTokenRequest") + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - request = build_communication_identity_exchange_teams_user_access_token_request( + _request = build_teams_user_operations_exchange_teams_user_access_token_request( content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, @@ -375,95 +630,386 @@ async def exchange_teams_user_access_token( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [200]: if _stream: - await response.read() # Load the body in memory and close the socket + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) - deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) if cls: - return cls(pipeline_response, deserialized, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + +class TeamsExtensionOperationsOperations: # pylint: disable=docstring-missing-param + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~azure.communication.identity.aio.IdentityClient`'s + :attr:`teams_extension_operations` attribute. + """ - return deserialized + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload - async def issue_access_token( - self, - id: str, - body: _models.CommunicationIdentityAccessTokenRequest, - *, - content_type: str = "application/json", - **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. + async def exchange_token( + self, body: _models.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. - Issue a new token for an identity. + Exchanges a Teams Phone token for an ACS user access token. - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest + :param body: Request payload for the token exchange. Required. + :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @overload - async def issue_access_token( - self, id: str, body: IO, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. + async def exchange_token( + self, body: _types.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. - Issue a new token for an identity. + Exchanges a Teams Phone token for an ACS user access token. - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: IO + :param body: Request payload for the token exchange. Required. + :type body: ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def exchange_token( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. + + Exchanges a Teams Phone token for an ACS user access token. + + :param body: Request payload for the token exchange. Required. + :type body: IO[bytes] :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace_async - async def issue_access_token( - self, id: str, body: Union[_models.CommunicationIdentityAccessTokenRequest, IO], **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. + async def exchange_token( + self, + body: Union[_models.TeamsExtensionExchangeTokenRequest, _types.TeamsExtensionExchangeTokenRequest, IO[bytes]], + **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. + + Exchanges a Teams Phone token for an ACS user access token. + + :param body: Request payload for the token exchange. Is either a + TeamsExtensionExchangeTokenRequest type or a IO[bytes] type. Required. + :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest + or ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest or + IO[bytes] + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) - Issue a new token for an identity. + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Is either a - CommunicationIdentityAccessTokenRequest type or a IO type. Required. - :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest or IO - :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. - Default value is None. + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_teams_extension_operations_exchange_token_request( + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @distributed_trace_async + async def get_assignment( + self, tenant_id: str, object_id: str, **kwargs: Any + ) -> _models.TeamsExtensionAssignmentResponse: + """Get Teams Phone access assignment by object id. + + Get Teams Phone access assignment by object id. + + :param tenant_id: Tenant Id we want to get the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to get the assignment for. Required. + :type object_id: str + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) + + _request = build_teams_extension_operations_get_assignment_request( + tenant_id=tenant_id, + object_id=object_id, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @overload + async def upsert_assignment( + self, + tenant_id: str, + object_id: str, + body: _models.TeamsExtensionAssignmentCreateOrUpdateRequest, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Required. + :type body: + ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def upsert_assignment( + self, + tenant_id: str, + object_id: str, + body: _types.TeamsExtensionAssignmentCreateOrUpdateRequest, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Required. + :type body: + ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def upsert_assignment( + self, tenant_id: str, object_id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace_async + async def upsert_assignment( + self, + tenant_id: str, + object_id: str, + body: Union[ + _models.TeamsExtensionAssignmentCreateOrUpdateRequest, + _types.TeamsExtensionAssignmentCreateOrUpdateRequest, + IO[bytes], + ], + **kwargs: Any + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Is either a TeamsExtensionAssignmentCreateOrUpdateRequest + type or a IO[bytes] type. Required. + :type body: + ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest + or ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest + or IO[bytes] + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -475,21 +1021,20 @@ async def issue_access_token( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) + cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) content_type = content_type or "application/json" - _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "CommunicationIdentityAccessTokenRequest") + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - request = build_communication_identity_issue_access_token_request( - id=id, + _request = build_teams_extension_operations_upsert_assignment_request( + tenant_id=tenant_id, + object_id=object_id, content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, @@ -497,25 +1042,92 @@ async def issue_access_token( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response - if response.status_code not in [200]: + if response.status_code not in [200, 201]: if _stream: - await response.read() # Load the body in memory and close the socket + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) - deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) if cls: - return cls(pipeline_response, deserialized, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @distributed_trace_async + async def delete_assignment(self, tenant_id: str, object_id: str, **kwargs: Any) -> None: + """Removes a Teams Phone access assignment. + + Removes a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to remove the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to remove the assignment for. Required. + :type object_id: str + :return: None + :rtype: None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) - return deserialized + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_teams_extension_operations_delete_assignment_request( + tenant_id=tenant_id, + object_id=object_id, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py index 9a71cff104ed..5daaa68dd7e1 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py @@ -2,22 +2,37 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +# pylint: disable=wrong-import-position -from ._models import CommunicationError -from ._models import CommunicationErrorResponse -from ._models import CommunicationIdentity -from ._models import CommunicationIdentityAccessToken -from ._models import CommunicationIdentityAccessTokenRequest -from ._models import CommunicationIdentityAccessTokenResult -from ._models import CommunicationIdentityCreateRequest -from ._models import TeamsUserExchangeTokenRequest +from typing import TYPE_CHECKING -from ._enums import CommunicationTokenScope +if TYPE_CHECKING: + from ._patch import * # pylint: disable=unused-wildcard-import + + +from ._models import ( # type: ignore + CommunicationError, + CommunicationErrorResponse, + CommunicationIdentity, + CommunicationIdentityAccessToken, + CommunicationIdentityAccessTokenRequest, + CommunicationIdentityAccessTokenResult, + CommunicationIdentityCreateRequest, + TeamsExtensionAssignmentCreateOrUpdateRequest, + TeamsExtensionAssignmentResponse, + TeamsExtensionExchangeTokenRequest, + TeamsUserExchangeTokenRequest, +) + +from ._enums import ( # type: ignore + CommunicationTokenScope, + TeamsExtensionPrincipalType, +) from ._patch import __all__ as _patch_all -from ._patch import * # pylint: disable=unused-wildcard-import +from ._patch import * from ._patch import patch_sdk as _patch_sdk __all__ = [ @@ -28,8 +43,12 @@ "CommunicationIdentityAccessTokenRequest", "CommunicationIdentityAccessTokenResult", "CommunicationIdentityCreateRequest", + "TeamsExtensionAssignmentCreateOrUpdateRequest", + "TeamsExtensionAssignmentResponse", + "TeamsExtensionExchangeTokenRequest", "TeamsUserExchangeTokenRequest", "CommunicationTokenScope", + "TeamsExtensionPrincipalType", ] -__all__.extend([p for p in _patch_all if p not in __all__]) +__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py index e95bddd653ed..5618e7dff5f6 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- @@ -21,6 +21,15 @@ class CommunicationTokenScope(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Access to Chat APIs but without the authorization to create, delete or update chat threads.""" CHAT_JOIN_LIMITED = "chat.join.limited" """A more limited version of chat.join that doesn't allow to add or remove participants. Use this - #: scope when the token bearer is not fully trusted, for example in guest scenarios.""" + scope when the token bearer is not fully trusted, for example in guest scenarios.""" VOIP_JOIN = "voip.join" """Access to Calling APIs but without the authorization to start new calls.""" + + +class TeamsExtensionPrincipalType(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """The type of principal the assignment is for.""" + + RESOURCE_ACCOUNT = "resourceAccount" + """Resource account principal type.""" + USER = "user" + """User principal type.""" diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py index a9f24485d60b..3d954eddc038 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py @@ -1,28 +1,24 @@ # coding=utf-8 -# pylint: disable=too-many-lines # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +# pylint: disable=useless-super-delegation -from typing import Any, List, Optional, TYPE_CHECKING, Union +import datetime +from typing import Any, Mapping, Optional, TYPE_CHECKING, Union, overload -from .. import _serialization +from .._utils.model_base import Model as _Model, rest_field if TYPE_CHECKING: - # pylint: disable=unused-import,ungrouped-imports from .. import models as _models -class CommunicationError(_serialization.Model): +class CommunicationError(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only """The Communication Services error. - Variables are only populated by the server, and will be ignored when sending a request. - - All required parameters must be populated in order to send to Azure. - :ivar code: The error code. Required. :vartype code: str :ivar message: The error message. Required. @@ -30,131 +26,134 @@ class CommunicationError(_serialization.Model): :ivar target: The error target. :vartype target: str :ivar details: Further details about specific errors that led to this error. - :vartype details: list[~azure.communication.identity.models.CommunicationError] + :vartype details: list[~azure.communication.identity._generated.models.CommunicationError] :ivar inner_error: The inner error if any. - :vartype inner_error: ~azure.communication.identity.models.CommunicationError + :vartype inner_error: ~azure.communication.identity._generated.models.CommunicationError """ - _validation = { - "code": {"required": True}, - "message": {"required": True}, - "target": {"readonly": True}, - "details": {"readonly": True}, - "inner_error": {"readonly": True}, - } - - _attribute_map = { - "code": {"key": "code", "type": "str"}, - "message": {"key": "message", "type": "str"}, - "target": {"key": "target", "type": "str"}, - "details": {"key": "details", "type": "[CommunicationError]"}, - "inner_error": {"key": "innererror", "type": "CommunicationError"}, - } - - def __init__(self, *, code: str, message: str, **kwargs: Any) -> None: + code: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The error code. Required.""" + message: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The error message. Required.""" + target: Optional[str] = rest_field(visibility=["read"]) + """The error target.""" + details: Optional[list["_models.CommunicationError"]] = rest_field(visibility=["read"]) + """Further details about specific errors that led to this error.""" + inner_error: Optional["_models.CommunicationError"] = rest_field(name="innererror", visibility=["read"]) + """The inner error if any.""" + + @overload + def __init__( + self, + *, + code: str, + message: str, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword code: The error code. Required. - :paramtype code: str - :keyword message: The error message. Required. - :paramtype message: str + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.code = code - self.message = message - self.target = None - self.details = None - self.inner_error = None + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class CommunicationErrorResponse(_serialization.Model): - """The Communication Services error. - All required parameters must be populated in order to send to Azure. +class CommunicationErrorResponse(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """The Communication Services error. :ivar error: The Communication Services error. Required. - :vartype error: ~azure.communication.identity.models.CommunicationError + :vartype error: ~azure.communication.identity._generated.models.CommunicationError """ - _validation = { - "error": {"required": True}, - } + error: "_models.CommunicationError" = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The Communication Services error. Required.""" - _attribute_map = { - "error": {"key": "error", "type": "CommunicationError"}, - } + @overload + def __init__( + self, + *, + error: "_models.CommunicationError", + ) -> None: ... - def __init__(self, *, error: "_models.CommunicationError", **kwargs: Any) -> None: + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword error: The Communication Services error. Required. - :paramtype error: ~azure.communication.identity.models.CommunicationError + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.error = error + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class CommunicationIdentity(_serialization.Model): - """A communication identity. - All required parameters must be populated in order to send to Azure. +class CommunicationIdentity(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """A communication identity. :ivar id: Identifier of the identity. Required. :vartype id: str """ - _validation = { - "id": {"required": True}, - } + id: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """Identifier of the identity. Required.""" - _attribute_map = { - "id": {"key": "id", "type": "str"}, - } + @overload + def __init__( + self, + *, + id: str, # pylint: disable=redefined-builtin + ) -> None: ... - def __init__(self, *, id: str, **kwargs: Any) -> None: # pylint: disable=redefined-builtin + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword id: Identifier of the identity. Required. - :paramtype id: str + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.id = id + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class CommunicationIdentityAccessToken(_serialization.Model): - """An access token. - All required parameters must be populated in order to send to Azure. +class CommunicationIdentityAccessToken(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """An access token. :ivar token: The access token issued for the identity. Required. :vartype token: str :ivar expires_on: The expiry time of the token. Required. - :vartype expires_on: str + :vartype expires_on: ~datetime.datetime """ - _validation = { - "token": {"required": True}, - "expires_on": {"required": True}, - } + token: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The access token issued for the identity. Required.""" + expires_on: datetime.datetime = rest_field( + name="expiresOn", visibility=["read", "create", "update", "delete", "query"], format="rfc3339" + ) + """The expiry time of the token. Required.""" - _attribute_map = { - "token": {"key": "token", "type": "str"}, - "expires_on": {"key": "expiresOn", "type": "str"}, - } + @overload + def __init__( + self, + *, + token: str, + expires_on: datetime.datetime, + ) -> None: ... - def __init__(self, *, token: str, expires_on: str, **kwargs: Any) -> None: + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword token: The access token issued for the identity. Required. - :paramtype token: str - :keyword expires_on: The expiry time of the token. Required. - :paramtype expires_on: str + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.token = token - self.expires_on = expires_on + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class CommunicationIdentityAccessTokenRequest(_serialization.Model): - """CommunicationIdentityAccessTokenRequest. - All required parameters must be populated in order to send to Azure. +class CommunicationIdentityAccessTokenRequest(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """Request to issue a new access token for an identity. :ivar scopes: List of scopes attached to the token. Required. :vartype scopes: list[str or ~azure.communication.identity.models.CommunicationTokenScope] @@ -163,78 +162,73 @@ class CommunicationIdentityAccessTokenRequest(_serialization.Model): :vartype expires_in_minutes: int """ - _validation = { - "scopes": {"required": True}, - "expires_in_minutes": {"maximum": 1440, "minimum": 60}, - } - - _attribute_map = { - "scopes": {"key": "scopes", "type": "[str]"}, - "expires_in_minutes": {"key": "expiresInMinutes", "type": "int"}, - } - + scopes: list[Union[str, "_models.CommunicationTokenScope"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """List of scopes attached to the token. Required.""" + expires_in_minutes: Optional[int] = rest_field( + name="expiresInMinutes", visibility=["read", "create", "update", "delete", "query"] + ) + """Optional custom validity period of the token within [60,1440] minutes range. If not provided, + the default value of 1440 minutes (24 hours) will be used.""" + + @overload def __init__( self, *, - scopes: List[Union[str, "_models.CommunicationTokenScope"]], - expires_in_minutes: int = 1440, - **kwargs: Any - ) -> None: + scopes: list[Union[str, "_models.CommunicationTokenScope"]], + expires_in_minutes: Optional[int] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword scopes: List of scopes attached to the token. Required. - :paramtype scopes: list[str or ~azure.communication.identity.models.CommunicationTokenScope] - :keyword expires_in_minutes: Optional custom validity period of the token within [60,1440] - minutes range. If not provided, the default value of 1440 minutes (24 hours) will be used. - :paramtype expires_in_minutes: int + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.scopes = scopes - self.expires_in_minutes = expires_in_minutes + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class CommunicationIdentityAccessTokenResult(_serialization.Model): - """A communication identity with access token. - All required parameters must be populated in order to send to Azure. +class CommunicationIdentityAccessTokenResult(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """A communication identity with access token. - :ivar identity: A communication identity. Required. - :vartype identity: ~azure.communication.identity.models.CommunicationIdentity + :ivar identity: The communication identity. Required. + :vartype identity: ~azure.communication.identity._generated.models.CommunicationIdentity :ivar access_token: An access token. - :vartype access_token: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :vartype access_token: + ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken """ - _validation = { - "identity": {"required": True}, - } - - _attribute_map = { - "identity": {"key": "identity", "type": "CommunicationIdentity"}, - "access_token": { - "key": "accessToken", - "type": "CommunicationIdentityAccessToken", - }, - } + identity: "_models.CommunicationIdentity" = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The communication identity. Required.""" + access_token: Optional["_models.CommunicationIdentityAccessToken"] = rest_field( + name="accessToken", visibility=["read", "create", "update", "delete", "query"] + ) + """An access token.""" + @overload def __init__( self, *, identity: "_models.CommunicationIdentity", access_token: Optional["_models.CommunicationIdentityAccessToken"] = None, - **kwargs: Any - ) -> None: + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword identity: A communication identity. Required. - :paramtype identity: ~azure.communication.identity.models.CommunicationIdentity - :keyword access_token: An access token. - :paramtype access_token: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.identity = identity - self.access_token = access_token + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class CommunicationIdentityCreateRequest(_serialization.Model): - """CommunicationIdentityCreateRequest. +class CommunicationIdentityCreateRequest(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """Request to create a new identity with optional access token. :ivar create_token_with_scopes: Also create access token for the created identity. :vartype create_token_with_scopes: list[str or @@ -244,76 +238,170 @@ class CommunicationIdentityCreateRequest(_serialization.Model): :vartype expires_in_minutes: int """ - _validation = { - "expires_in_minutes": {"maximum": 1440, "minimum": 60}, - } + create_token_with_scopes: Optional[list[Union[str, "_models.CommunicationTokenScope"]]] = rest_field( + name="createTokenWithScopes", visibility=["read", "create", "update", "delete", "query"] + ) + """Also create access token for the created identity.""" + expires_in_minutes: Optional[int] = rest_field( + name="expiresInMinutes", visibility=["read", "create", "update", "delete", "query"] + ) + """Optional custom validity period of the token within [60,1440] minutes range. If not provided, + the default value of 1440 minutes (24 hours) will be used.""" + + @overload + def __init__( + self, + *, + create_token_with_scopes: Optional[list[Union[str, "_models.CommunicationTokenScope"]]] = None, + expires_in_minutes: Optional[int] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + - _attribute_map = { - "create_token_with_scopes": {"key": "createTokenWithScopes", "type": "[str]"}, - "expires_in_minutes": {"key": "expiresInMinutes", "type": "int"}, - } +class TeamsExtensionAssignmentCreateOrUpdateRequest( + _Model +): # pylint: disable=name-too-long,docstring-keyword-should-match-keyword-only + """A request to create or update a Teams Extension assignment. + :ivar principal_type: The type of principal the assignment is for. Required. Known values are: + "resourceAccount" and "user". + :vartype principal_type: str or + ~azure.communication.identity.models.TeamsExtensionPrincipalType + :ivar client_ids: The client IDs for the assignment. + :vartype client_ids: list[str] + """ + + principal_type: Union[str, "_models.TeamsExtensionPrincipalType"] = rest_field( + name="principalType", visibility=["read", "create", "update", "delete", "query"] + ) + """The type of principal the assignment is for. Required. Known values are: \"resourceAccount\" + and \"user\".""" + client_ids: Optional[list[str]] = rest_field( + name="clientIds", visibility=["read", "create", "update", "delete", "query"] + ) + """The client IDs for the assignment.""" + + @overload def __init__( self, *, - create_token_with_scopes: Optional[List[Union[str, "_models.CommunicationTokenScope"]]] = None, - expires_in_minutes: int = 1440, - **kwargs: Any - ) -> None: + principal_type: Union[str, "_models.TeamsExtensionPrincipalType"], + client_ids: Optional[list[str]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword create_token_with_scopes: Also create access token for the created identity. - :paramtype create_token_with_scopes: list[str or - ~azure.communication.identity.models.CommunicationTokenScope] - :keyword expires_in_minutes: Optional custom validity period of the token within [60,1440] - minutes range. If not provided, the default value of 1440 minutes (24 hours) will be used. - :paramtype expires_in_minutes: int + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.create_token_with_scopes = create_token_with_scopes - self.expires_in_minutes = expires_in_minutes + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) -class TeamsUserExchangeTokenRequest(_serialization.Model): - """TeamsUserExchangeTokenRequest. - All required parameters must be populated in order to send to Azure. +class TeamsExtensionAssignmentResponse(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """A Teams Extension assignment response. - :ivar token: Azure AD access token of a Teams User to acquire a new Communication Identity + :ivar object_id: The object ID of the assignment. Required. + :vartype object_id: str + :ivar tenant_id: The tenant ID of the assignment. Required. + :vartype tenant_id: str + :ivar principal_type: The type of principal the assignment is for. Required. Known values are: + "resourceAccount" and "user". + :vartype principal_type: str or + ~azure.communication.identity.models.TeamsExtensionPrincipalType + :ivar client_ids: The client IDs for the assignment. + :vartype client_ids: list[str] + """ + + object_id: str = rest_field(name="objectId", visibility=["read", "create", "update", "delete", "query"]) + """The object ID of the assignment. Required.""" + tenant_id: str = rest_field(name="tenantId", visibility=["read", "create", "update", "delete", "query"]) + """The tenant ID of the assignment. Required.""" + principal_type: Union[str, "_models.TeamsExtensionPrincipalType"] = rest_field( + name="principalType", visibility=["read", "create", "update", "delete", "query"] + ) + """The type of principal the assignment is for. Required. Known values are: \"resourceAccount\" + and \"user\".""" + client_ids: Optional[list[str]] = rest_field( + name="clientIds", visibility=["read", "create", "update", "delete", "query"] + ) + """The client IDs for the assignment.""" + + @overload + def __init__( + self, + *, + object_id: str, + tenant_id: str, + principal_type: Union[str, "_models.TeamsExtensionPrincipalType"], + client_ids: Optional[list[str]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class TeamsExtensionExchangeTokenRequest(_Model): + """A request to exchange a Teams Extension token.""" + + +class TeamsUserExchangeTokenRequest(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only + """A request to exchange a Teams user access token. + + :ivar token: Entra ID access token of a Teams User to acquire a new Communication Identity access token. Required. :vartype token: str - :ivar app_id: Client ID of an Azure AD application to be verified against the appid claim in - the Azure AD access token. Required. + :ivar app_id: Client ID of an Entra ID application to be verified against the appid claim in + the Entra ID access token. Required. :vartype app_id: str - :ivar user_id: Object ID of an Azure AD user (Teams User) to be verified against the oid claim - in the Azure AD access token. Required. + :ivar user_id: Object ID of an Entra ID user (Teams User) to be verified against the oid claim + in the Entra ID access token. Required. :vartype user_id: str """ - _validation = { - "token": {"required": True}, - "app_id": {"required": True}, - "user_id": {"required": True}, - } - - _attribute_map = { - "token": {"key": "token", "type": "str"}, - "app_id": {"key": "appId", "type": "str"}, - "user_id": {"key": "userId", "type": "str"}, - } + token: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """Entra ID access token of a Teams User to acquire a new Communication Identity access token. + Required.""" + app_id: str = rest_field(name="appId", visibility=["read", "create", "update", "delete", "query"]) + """Client ID of an Entra ID application to be verified against the appid claim in the Entra ID + access token. Required.""" + user_id: str = rest_field(name="userId", visibility=["read", "create", "update", "delete", "query"]) + """Object ID of an Entra ID user (Teams User) to be verified against the oid claim in the Entra ID + access token. Required.""" + + @overload + def __init__( + self, + *, + token: str, + app_id: str, + user_id: str, + ) -> None: ... - def __init__(self, *, token: str, app_id: str, user_id: str, **kwargs: Any) -> None: + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: """ - :keyword token: Azure AD access token of a Teams User to acquire a new Communication Identity - access token. Required. - :paramtype token: str - :keyword app_id: Client ID of an Azure AD application to be verified against the appid claim in - the Azure AD access token. Required. - :paramtype app_id: str - :keyword user_id: Object ID of an Azure AD user (Teams User) to be verified against the oid - claim in the Azure AD access token. Required. - :paramtype user_id: str + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] """ - super().__init__(**kwargs) - self.token = token - self.app_id = app_id - self.user_id = user_id + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py index 2c8ddc0b3dd9..46690414aedf 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py @@ -2,18 +2,28 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +# pylint: disable=wrong-import-position -from ._operations import CommunicationIdentityOperations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._patch import * # pylint: disable=unused-wildcard-import + +from ._operations import IdentityOperationsOperations # type: ignore +from ._operations import TeamsUserOperationsOperations # type: ignore +from ._operations import TeamsExtensionOperationsOperations # type: ignore from ._patch import __all__ as _patch_all -from ._patch import * # pylint: disable=unused-wildcard-import +from ._patch import * from ._patch import patch_sdk as _patch_sdk __all__ = [ - "CommunicationIdentityOperations", + "IdentityOperationsOperations", + "TeamsUserOperationsOperations", + "TeamsExtensionOperationsOperations", ] -__all__.extend([p for p in _patch_all if p not in __all__]) +__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py index a60d0262ba38..b3495d1ac211 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py @@ -3,18 +3,23 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from collections.abc import MutableMapping from io import IOBase -from typing import Any, Callable, Dict, IO, Optional, TypeVar, Union, overload +import json +from typing import Any, Callable, IO, Optional, TypeVar, Union, overload +from azure.core import PipelineClient from azure.core.exceptions import ( ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, ResourceNotModifiedError, + StreamClosedError, + StreamConsumedError, map_error, ) from azure.core.pipeline import PipelineResponse @@ -22,24 +27,24 @@ from azure.core.tracing.decorator import distributed_trace from azure.core.utils import case_insensitive_dict -from .. import models as _models -from .._serialization import Serializer +from .. import models as _models, types as _types +from .._configuration import IdentityClientConfiguration +from .._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize +from .._utils.serialization import Deserializer, Serializer T = TypeVar("T") -ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, dict[str, Any]], Any]] _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False -def build_communication_identity_create_request( - **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +def build_identity_operations_create_request(**kwargs: Any) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -56,13 +61,10 @@ def build_communication_identity_create_request( return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -def build_communication_identity_delete_request(id: str, **kwargs: Any) -> HttpRequest: # pylint: disable=name-too-long - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) +def build_identity_operations_delete_request(id: str, **kwargs: Any) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) - accept = _headers.pop("Accept", "application/json") - + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) # Construct URL _url = "/identities/{id}" path_format_arguments = { @@ -74,23 +76,41 @@ def build_communication_identity_delete_request(id: str, **kwargs: Any) -> HttpR # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - # Construct headers - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + return HttpRequest(method="DELETE", url=_url, params=_params, **kwargs) - return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) +def build_identity_operations_revoke_access_tokens_request( # pylint: disable=name-too-long + id: str, **kwargs: Any +) -> HttpRequest: + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + # Construct URL + _url = "/identities/{id}/:revokeAccessTokens" + path_format_arguments = { + "id": _SERIALIZER.url("id", id, "str"), + } -def build_communication_identity_revoke_access_tokens_request( # pylint: disable=name-too-long + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + return HttpRequest(method="POST", url=_url, params=_params, **kwargs) + + +def build_identity_operations_issue_access_token_request( # pylint: disable=name-too-long id: str, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) accept = _headers.pop("Accept", "application/json") # Construct URL - _url = "/identities/{id}/:revokeAccessTokens" + _url = "/identities/{id}/:issueAccessToken" path_format_arguments = { "id": _SERIALIZER.url("id", id, "str"), } @@ -101,19 +121,21 @@ def build_communication_identity_revoke_access_tokens_request( # pylint: disabl _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -def build_communication_identity_exchange_teams_user_access_token_request( # pylint: disable=name-too-long +def build_teams_user_operations_exchange_teams_user_access_token_request( # pylint: disable=name-too-long **kwargs: Any, ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -130,20 +152,72 @@ def build_communication_identity_exchange_teams_user_access_token_request( # py return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -def build_communication_identity_issue_access_token_request( # pylint: disable=name-too-long - id: str, **kwargs: Any +def build_teams_extension_operations_exchange_token_request( # pylint: disable=name-too-long + **kwargs: Any, ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) accept = _headers.pop("Accept", "application/json") # Construct URL - _url = "/identities/{id}/:issueAccessToken" + _url = "/access/teamsExtension/:exchangeAccessToken" + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_teams_extension_operations_get_assignment_request( # pylint: disable=name-too-long + tenant_id: str, object_id: str, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/access/teamsExtension/tenants/{tenantId}/assignments/{objectId}" path_format_arguments = { - "id": _SERIALIZER.url("id", id, "str"), + "tenantId": _SERIALIZER.url("tenant_id", tenant_id, "str"), + "objectId": _SERIALIZER.url("object_id", object_id, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_teams_extension_operations_upsert_assignment_request( # pylint: disable=name-too-long + tenant_id: str, object_id: str, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/access/teamsExtension/tenants/{tenantId}/assignments/{objectId}" + path_format_arguments = { + "tenantId": _SERIALIZER.url("tenant_id", tenant_id, "str"), + "objectId": _SERIALIZER.url("object_id", object_id, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -156,27 +230,46 @@ def build_communication_identity_issue_access_token_request( # pylint: disable= _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) -class CommunicationIdentityOperations: +def build_teams_extension_operations_delete_assignment_request( # pylint: disable=name-too-long + tenant_id: str, object_id: str, **kwargs: Any +) -> HttpRequest: + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + # Construct URL + _url = "/access/teamsExtension/tenants/{tenantId}/assignments/{objectId}" + path_format_arguments = { + "tenantId": _SERIALIZER.url("tenant_id", tenant_id, "str"), + "objectId": _SERIALIZER.url("object_id", object_id, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + return HttpRequest(method="DELETE", url=_url, params=_params, **kwargs) + + +class IdentityOperationsOperations: # pylint: disable=docstring-missing-param """ .. warning:: **DO NOT** instantiate this class directly. Instead, you should access the following operations through - :class:`~azure.communication.identity.CommunicationIdentityClient`'s - :attr:`communication_identity` attribute. + :class:`~azure.communication.identity.IdentityClient`'s + :attr:`identity_operations` attribute. """ - models = _models - - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: input_args = list(args) - self._client = input_args.pop(0) if input_args else kwargs.pop("client") - self._config = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize = input_args.pop(0) if input_args else kwargs.pop("deserializer") + self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload def create( @@ -192,19 +285,20 @@ def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest + :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @overload def create( self, - body: Optional[IO] = None, + body: Optional[_types.CommunicationIdentityCreateRequest] = None, *, content_type: str = "application/json", **kwargs: Any, @@ -215,19 +309,42 @@ def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: IO + :type body: ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def create( + self, body: Optional[IO[bytes]] = None, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Create a new identity, and optionally, an access token. + + Create a new identity, and optionally, an access token. + + :param body: If specified, creates also a Communication Identity access token associated with + the identity and containing the requested scopes. Default value is None. + :type body: IO[bytes] :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace def create( self, - body: Optional[Union[_models.CommunicationIdentityCreateRequest, IO]] = None, + body: Optional[ + Union[_models.CommunicationIdentityCreateRequest, _types.CommunicationIdentityCreateRequest, IO[bytes]] + ] = None, **kwargs: Any, ) -> _models.CommunicationIdentityAccessTokenResult: """Create a new identity, and optionally, an access token. @@ -236,16 +353,16 @@ def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Is either a - CommunicationIdentityCreateRequest type or a IO type. Default value is None. - :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest or IO - :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. - Default value is None. - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult + CommunicationIdentityCreateRequest type or a IO[bytes] type. Default value is None. + :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest + or ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest or + IO[bytes] + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -257,23 +374,22 @@ def create( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + content_type = content_type if body else None cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) - content_type = content_type or "application/json" - _json = None + content_type = content_type or "application/json" if body else None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: if body is not None: - _json = self._serialize.body(body, "CommunicationIdentityCreateRequest") + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore else: - _json = None + _content = None - request = build_communication_identity_create_request( + _request = build_identity_operations_create_request( content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, @@ -281,28 +397,38 @@ def create( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [201]: if _stream: - response.read() # Load the body in memory and close the socket + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) - deserialized = self._deserialize("CommunicationIdentityAccessTokenResult", pipeline_response) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) if cls: - return cls(pipeline_response, deserialized, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore - return deserialized + return deserialized # type: ignore @distributed_trace def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements @@ -316,7 +442,7 @@ def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsisten :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -329,7 +455,7 @@ def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsisten cls: ClsType[None] = kwargs.pop("cls", None) - request = build_communication_identity_delete_request( + _request = build_identity_operations_delete_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -338,24 +464,25 @@ def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsisten path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: - if _stream: - response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) if cls: - return cls(pipeline_response, None, {}) + return cls(pipeline_response, None, {}) # type: ignore @distributed_trace def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements @@ -369,7 +496,7 @@ def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disab :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -382,7 +509,7 @@ def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disab cls: ClsType[None] = kwargs.pop("cls", None) - request = build_communication_identity_revoke_access_tokens_request( + _request = build_identity_operations_revoke_access_tokens_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -391,90 +518,295 @@ def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disab path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: - if _stream: - response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) if cls: - return cls(pipeline_response, None, {}) + return cls(pipeline_response, None, {}) # type: ignore @overload - def exchange_teams_user_access_token( + def issue_access_token( + self, + id: str, + body: _models.CommunicationIdentityAccessTokenRequest, + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: + ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def issue_access_token( self, - body: _models.TeamsUserExchangeTokenRequest, + id: str, + body: _types.CommunicationIdentityAccessTokenRequest, *, content_type: str = "application/json", **kwargs: Any, ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: + ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def issue_access_token( + self, id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace + def issue_access_token( + self, + id: str, + body: Union[ + _models.CommunicationIdentityAccessTokenRequest, _types.CommunicationIdentityAccessTokenRequest, IO[bytes] + ], + **kwargs: Any, + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Is either a + CommunicationIdentityAccessTokenRequest type or a IO[bytes] type. Required. + :type body: + ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest or + ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest or + IO[bytes] + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_identity_operations_issue_access_token_request( + id=id, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) - Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + +class TeamsUserOperationsOperations: # pylint: disable=docstring-missing-param + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~azure.communication.identity.IdentityClient`'s + :attr:`teams_user_operations` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") + + @overload + def exchange_teams_user_access_token( + self, body: _models.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. + + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. + + :param body: Request payload for the token exchange. Required. + :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def exchange_teams_user_access_token( + self, body: _types.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. + + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest + :type body: ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @overload def exchange_teams_user_access_token( - self, body: IO, *, content_type: str = "application/json", **kwargs: Any + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. - Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: IO + :type body: IO[bytes] :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace def exchange_teams_user_access_token( - self, body: Union[_models.TeamsUserExchangeTokenRequest, IO], **kwargs: Any + self, + body: Union[_models.TeamsUserExchangeTokenRequest, _types.TeamsUserExchangeTokenRequest, IO[bytes]], + **kwargs: Any, ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. - Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new - Communication Identity access token with a matching expiration time. + Exchange an Entra ID access token of a Teams user for a new Communication Identity access token + with a matching expiration time. :param body: Request payload for the token exchange. Is either a TeamsUserExchangeTokenRequest - type or a IO type. Required. - :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest or IO - :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. - Default value is None. - :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + type or a IO[bytes] type. Required. + :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest or + ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest or IO[bytes] + :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -489,17 +821,15 @@ def exchange_teams_user_access_token( cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) content_type = content_type or "application/json" - _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "TeamsUserExchangeTokenRequest") + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - request = build_communication_identity_exchange_teams_user_access_token_request( + _request = build_teams_user_operations_exchange_teams_user_access_token_request( content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, @@ -507,103 +837,384 @@ def exchange_teams_user_access_token( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [200]: if _stream: - response.read() # Load the body in memory and close the socket + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) - deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) if cls: - return cls(pipeline_response, deserialized, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + +class TeamsExtensionOperationsOperations: # pylint: disable=docstring-missing-param + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~azure.communication.identity.IdentityClient`'s + :attr:`teams_extension_operations` attribute. + """ - return deserialized + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload - def issue_access_token( + def exchange_token( + self, body: _models.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. + + Exchanges a Teams Phone token for an ACS user access token. + + :param body: Request payload for the token exchange. Required. + :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def exchange_token( + self, body: _types.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. + + Exchanges a Teams Phone token for an ACS user access token. + + :param body: Request payload for the token exchange. Required. + :type body: ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def exchange_token( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. + + Exchanges a Teams Phone token for an ACS user access token. + + :param body: Request payload for the token exchange. Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace + def exchange_token( self, - id: str, - body: _models.CommunicationIdentityAccessTokenRequest, - *, - content_type: str = "application/json", + body: Union[_models.TeamsExtensionExchangeTokenRequest, _types.TeamsExtensionExchangeTokenRequest, IO[bytes]], **kwargs: Any, - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. + ) -> _models.CommunicationIdentityAccessTokenResult: + """Exchanges a Teams Phone token for an ACS user access token. + + Exchanges a Teams Phone token for an ACS user access token. + + :param body: Request payload for the token exchange. Is either a + TeamsExtensionExchangeTokenRequest type or a IO[bytes] type. Required. + :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest + or ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest or + IO[bytes] + :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is + compatible with MutableMapping + :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) - Issue a new token for an identity. + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_teams_extension_operations_exchange_token_request( + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @distributed_trace + def get_assignment(self, tenant_id: str, object_id: str, **kwargs: Any) -> _models.TeamsExtensionAssignmentResponse: + """Get Teams Phone access assignment by object id. + + Get Teams Phone access assignment by object id. + + :param tenant_id: Tenant Id we want to get the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to get the assignment for. Required. + :type object_id: str + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) + + _request = build_teams_extension_operations_get_assignment_request( + tenant_id=tenant_id, + object_id=object_id, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @overload + def upsert_assignment( + self, + tenant_id: str, + object_id: str, + body: _models.TeamsExtensionAssignmentCreateOrUpdateRequest, + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Required. + :type body: + ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse :raises ~azure.core.exceptions.HttpResponseError: """ @overload - def issue_access_token( + def upsert_assignment( self, - id: str, - body: IO, + tenant_id: str, + object_id: str, + body: _types.TeamsExtensionAssignmentCreateOrUpdateRequest, *, content_type: str = "application/json", **kwargs: Any, - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Required. + :type body: + ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :raises ~azure.core.exceptions.HttpResponseError: + """ - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: IO + @overload + def upsert_assignment( + self, tenant_id: str, object_id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Required. + :type body: IO[bytes] :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace - def issue_access_token( + def upsert_assignment( self, - id: str, - body: Union[_models.CommunicationIdentityAccessTokenRequest, IO], + tenant_id: str, + object_id: str, + body: Union[ + _models.TeamsExtensionAssignmentCreateOrUpdateRequest, + _types.TeamsExtensionAssignmentCreateOrUpdateRequest, + IO[bytes], + ], **kwargs: Any, - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Is either a - CommunicationIdentityAccessTokenRequest type or a IO type. Required. - :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest or IO - :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. - Default value is None. - :paramtype content_type: str - :return: CommunicationIdentityAccessToken - :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken + ) -> _models.TeamsExtensionAssignmentResponse: + """Creates or replaces a Teams Phone access assignment. + + Creates or replaces a Teams Phone access assignment. + + :param tenant_id: Tenant Id we want to update the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to update the assignment for. Required. + :type object_id: str + :param body: Values we want to set. Is either a TeamsExtensionAssignmentCreateOrUpdateRequest + type or a IO[bytes] type. Required. + :type body: + ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest + or ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest + or IO[bytes] + :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible + with MutableMapping + :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse :raises ~azure.core.exceptions.HttpResponseError: """ - error_map = { + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -615,21 +1226,20 @@ def issue_access_token( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) + cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) content_type = content_type or "application/json" - _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "CommunicationIdentityAccessTokenRequest") + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - request = build_communication_identity_issue_access_token_request( - id=id, + _request = build_teams_extension_operations_upsert_assignment_request( + tenant_id=tenant_id, + object_id=object_id, content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, @@ -637,25 +1247,94 @@ def issue_access_token( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - request.url = self._client.format_url(request.url, **path_format_arguments) + _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - request, stream=_stream, **kwargs + _request, stream=_stream, **kwargs ) response = pipeline_response.http_response - if response.status_code not in [200]: + if response.status_code not in [200, 201]: if _stream: - response.read() # Load the body in memory and close the socket + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) raise HttpResponseError(response=response, model=error) - deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) if cls: - return cls(pipeline_response, deserialized, {}) + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @distributed_trace + def delete_assignment( # pylint: disable=inconsistent-return-statements + self, tenant_id: str, object_id: str, **kwargs: Any + ) -> None: + """Removes a Teams Phone access assignment. + + Removes a Teams Phone access assignment. - return deserialized + :param tenant_id: Tenant Id we want to remove the assignment for. Required. + :type tenant_id: str + :param object_id: Object Id we want to remove the assignment for. Required. + :type object_id: str + :return: None + :rtype: None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_teams_extension_operations_delete_assignment_request( + tenant_id=tenant_id, + object_id=object_id, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.CommunicationErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed deleted file mode 100644 index e5aff4f83af8..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed +++ /dev/null @@ -1 +0,0 @@ -# Marker file for PEP 561. \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py new file mode 100644 index 000000000000..3eb0053aa283 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py @@ -0,0 +1,93 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# -------------------------------------------------------------------------- + +from typing import TYPE_CHECKING, Union +from typing_extensions import Required, TypedDict + +if TYPE_CHECKING: + from .models import CommunicationTokenScope, TeamsExtensionPrincipalType + + +class CommunicationIdentityAccessTokenRequest(TypedDict, total=False): + """Request to issue a new access token for an identity. + + :ivar scopes: List of scopes attached to the token. Required. + :vartype scopes: list[Union[str, "CommunicationTokenScope"]] + :ivar expiresInMinutes: Optional custom validity period of the token within [60,1440] minutes + range. If not provided, the default value of 1440 minutes (24 hours) will be used. + :vartype expiresInMinutes: int + """ + + scopes: Required[list[Union[str, "CommunicationTokenScope"]]] + """List of scopes attached to the token. Required.""" + expiresInMinutes: int + """Optional custom validity period of the token within [60,1440] minutes range. If not provided, + the default value of 1440 minutes (24 hours) will be used.""" + + +class CommunicationIdentityCreateRequest(TypedDict, total=False): + """Request to create a new identity with optional access token. + + :ivar createTokenWithScopes: Also create access token for the created identity. + :vartype createTokenWithScopes: list[Union[str, "CommunicationTokenScope"]] + :ivar expiresInMinutes: Optional custom validity period of the token within [60,1440] minutes + range. If not provided, the default value of 1440 minutes (24 hours) will be used. + :vartype expiresInMinutes: int + """ + + createTokenWithScopes: list[Union[str, "CommunicationTokenScope"]] + """Also create access token for the created identity.""" + expiresInMinutes: int + """Optional custom validity period of the token within [60,1440] minutes range. If not provided, + the default value of 1440 minutes (24 hours) will be used.""" + + +class TeamsExtensionAssignmentCreateOrUpdateRequest(TypedDict, total=False): # pylint: disable=name-too-long + """A request to create or update a Teams Extension assignment. + + :ivar principalType: The type of principal the assignment is for. Required. Known values are: + "resourceAccount" and "user". + :vartype principalType: Union[str, "TeamsExtensionPrincipalType"] + :ivar clientIds: The client IDs for the assignment. + :vartype clientIds: list[str] + """ + + principalType: Required[Union[str, "TeamsExtensionPrincipalType"]] + """The type of principal the assignment is for. Required. Known values are: \"resourceAccount\" + and \"user\".""" + clientIds: list[str] + """The client IDs for the assignment.""" + + +class TeamsExtensionExchangeTokenRequest(TypedDict, total=False): + """A request to exchange a Teams Extension token.""" + + +class TeamsUserExchangeTokenRequest(TypedDict, total=False): + """A request to exchange a Teams user access token. + + :ivar token: Entra ID access token of a Teams User to acquire a new Communication Identity + access token. Required. + :vartype token: str + :ivar appId: Client ID of an Entra ID application to be verified against the appid claim in the + Entra ID access token. Required. + :vartype appId: str + :ivar userId: Object ID of an Entra ID user (Teams User) to be verified against the oid claim + in the Entra ID access token. Required. + :vartype userId: str + """ + + token: Required[str] + """Entra ID access token of a Teams User to acquire a new Communication Identity access token. + Required.""" + appId: Required[str] + """Client ID of an Entra ID application to be verified against the appid claim in the Entra ID + access token. Required.""" + userId: Required[str] + """Object ID of an Entra ID user (Teams User) to be verified against the oid claim in the Entra ID + access token. Required.""" diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py deleted file mode 100644 index c176b112f9b1..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py +++ /dev/null @@ -1,53 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - -from typing import Union, cast -from azure.core.credentials import TokenCredential, AzureKeyCredential -from azure.core.credentials_async import AsyncTokenCredential -from azure.core.pipeline.policies import ( - AsyncBearerTokenCredentialPolicy, - BearerTokenCredentialPolicy, -) -from .._shared.policy import HMACCredentialsPolicy - - -def get_authentication_policy( - endpoint: str, - credential: Union[TokenCredential, AsyncTokenCredential, AzureKeyCredential, str], - decode_url: bool = False, - is_async: bool = False, -) -> Union[AsyncBearerTokenCredentialPolicy, BearerTokenCredentialPolicy, HMACCredentialsPolicy]: - """Returns the correct authentication policy based on which credential is being passed. - - :param endpoint: The endpoint to which we are authenticating to. - :type endpoint: str - :param credential: The credential we use to authenticate to the service - :type credential: Union[TokenCredential, AsyncTokenCredential, AzureKeyCredential, str] - :param bool decode_url: `True` if there is a need to decode the url. Default value is `False` - :param bool is_async: For async clients there is a need to decode the url - :return: The authentication policy to be used. - :rtype: ~azure.core.pipeline.policies.AsyncBearerTokenCredentialPolicy or - ~azure.core.pipeline.policies.BearerTokenCredentialPolicy or - ~.HMACCredentialsPolicy - """ - - if credential is None: - raise ValueError("Parameter 'credential' must not be None.") - if hasattr(credential, "get_token"): - if is_async: - return AsyncBearerTokenCredentialPolicy( - cast(AsyncTokenCredential, credential), "https://communication.azure.com//.default" - ) - return BearerTokenCredentialPolicy( - cast(TokenCredential, credential), "https://communication.azure.com//.default" - ) - if isinstance(credential, (AzureKeyCredential, str)): - return HMACCredentialsPolicy(endpoint, credential, decode_url=decode_url) - - raise TypeError( - f"Unsupported credential: {format(type(credential))}. Use an access token string to use HMACCredentialsPolicy" - "or a token credential from azure.identity" - ) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py deleted file mode 100644 index 651378179892..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py +++ /dev/null @@ -1,30 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline import PipelineRequest -from . import token_utils - - -class EntraTokenGuardPolicy(HTTPPolicy): - """A pipeline policy that caches the response for a given Entra token and reuses it if valid.""" - - def __init__(self): - super().__init__() - self._entra_token_cache = None - self._response_cache = None - - def send(self, request: PipelineRequest): - cache_valid, token = token_utils.is_entra_token_cache_valid(self._entra_token_cache, request) - if cache_valid and token_utils.is_acs_token_cache_valid(self._response_cache): - response = self._response_cache - else: - self._entra_token_cache = token - response = self.next.send(request) - self._response_cache = response - if response is None: - raise RuntimeError("Failed to obtain a valid PipelineResponse in EntraTokenGuardPolicy.send") - return response diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py deleted file mode 100644 index 195324bbe28e..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py +++ /dev/null @@ -1,30 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -from azure.core.pipeline.policies import AsyncHTTPPolicy -from azure.core.pipeline import PipelineRequest -from . import token_utils - - -class EntraTokenGuardPolicy(AsyncHTTPPolicy): - """Async pipeline policy that caches the response for a given Entra token and reuses it if valid.""" - - def __init__(self): - super().__init__() - self._entra_token_cache = None - self._response_cache = None - - async def send(self, request: PipelineRequest): - cache_valid, token = token_utils.is_entra_token_cache_valid(self._entra_token_cache, request) - if cache_valid and token_utils.is_acs_token_cache_valid(self._response_cache): - response = self._response_cache - else: - self._entra_token_cache = token - response = await self.next.send(request) - self._response_cache = response - if response is None: - raise RuntimeError("Failed to obtain a valid PipelineResponse in AsyncEntraTokenGuardPolicy.send") - return response diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py deleted file mode 100644 index d71d8428504d..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py +++ /dev/null @@ -1,522 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from enum import Enum -import warnings -from typing import Mapping, Optional, Union, Any, cast -from typing_extensions import Literal, TypedDict, Protocol, runtime_checkable, NotRequired - -from azure.core import CaseInsensitiveEnumMeta - - -class DeprecatedEnumMeta(CaseInsensitiveEnumMeta): - - def __getattribute__(cls, item): - if item.upper() == "MICROSOFT_BOT": - warnings.warn( - "MICROSOFT_BOT is deprecated and has been replaced by \ - MICROSOFT_TEAMS_APP identifier.", - DeprecationWarning, - ) - item = "MICROSOFT_TEAMS_APP" - return super().__getattribute__(item) - - -class CommunicationIdentifierKind(str, Enum, metaclass=DeprecatedEnumMeta): - """Communication Identifier Kind. - - For checking yet unknown identifiers it is better to rely on the presence of the `raw_id` property, - as new or existing distinct type identifiers always contain the `raw_id` property. - It is not advisable to rely on the `kind` property with a value `unknown`, - as it could become a new or existing distinct type in the future. - """ - - UNKNOWN = "unknown" - COMMUNICATION_USER = "communication_user" - PHONE_NUMBER = "phone_number" - MICROSOFT_TEAMS_USER = "microsoft_teams_user" - MICROSOFT_TEAMS_APP = "microsoft_teams_app" - TEAMS_EXTENSION_USER = "teams_extension_user" - - -class CommunicationCloudEnvironment(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """The cloud environment that the identifier belongs to""" - - PUBLIC = "PUBLIC" - DOD = "DOD" - GCCH = "GCCH" - - -@runtime_checkable -class CommunicationIdentifier(Protocol): - """Communication Identifier.""" - - @property - def raw_id(self) -> str: - """The raw ID of the identifier.""" - ... - - @property - def kind(self) -> CommunicationIdentifierKind: - """The type of identifier.""" - ... - - @property - def properties(self) -> Mapping[str, Any]: - """The properties of the identifier.""" - ... - - -PHONE_NUMBER_PREFIX = "4:" -BOT_PREFIX = "28:" -BOT_PUBLIC_CLOUD_PREFIX = "28:orgid:" -BOT_DOD_CLOUD_PREFIX = "28:dod:" -BOT_DOD_CLOUD_GLOBAL_PREFIX = "28:dod-global:" -BOT_GCCH_CLOUD_PREFIX = "28:gcch:" -BOT_GCCH_CLOUD_GLOBAL_PREFIX = "28:gcch-global:" -TEAMS_APP_PUBLIC_CLOUD_PREFIX = "28:orgid:" -TEAMS_APP_DOD_CLOUD_PREFIX = "28:dod:" -TEAMS_APP_GCCH_CLOUD_PREFIX = "28:gcch:" -TEAMS_USER_ANONYMOUS_PREFIX = "8:teamsvisitor:" -TEAMS_USER_PUBLIC_CLOUD_PREFIX = "8:orgid:" -TEAMS_USER_DOD_CLOUD_PREFIX = "8:dod:" -TEAMS_USER_GCCH_CLOUD_PREFIX = "8:gcch:" -ACS_USER_PREFIX = "8:acs:" -ACS_USER_DOD_CLOUD_PREFIX = "8:dod-acs:" -ACS_USER_GCCH_CLOUD_PREFIX = "8:gcch-acs:" -SPOOL_USER_PREFIX = "8:spool:" - -PHONE_NUMBER_ANONYMOUS_SUFFIX = "anonymous" - - -class CommunicationUserProperties(TypedDict): - """Dictionary of properties for a CommunicationUserIdentifier.""" - - id: str - """ID of the Communication user as returned from Azure Communication Identity.""" - - -class CommunicationUserIdentifier: - """Represents a user in Azure Communication Service.""" - - kind: Literal[CommunicationIdentifierKind.COMMUNICATION_USER] = CommunicationIdentifierKind.COMMUNICATION_USER - """The type of identifier.""" - properties: CommunicationUserProperties - """The properties of the identifier.""" - raw_id: str - """The raw ID of the identifier.""" - - def __init__(self, id: str, **kwargs: Any) -> None: - """ - :param str id: ID of the Communication user as returned from Azure Communication Identity. - :keyword str raw_id: The raw ID of the identifier. If not specified, the 'id' value will be used. - """ - self.properties = CommunicationUserProperties(id=id) - raw_id: Optional[str] = kwargs.get("raw_id") - self.raw_id = raw_id if raw_id is not None else id - - def __eq__(self, other): - try: - if other.raw_id: - return self.raw_id == other.raw_id - return self.raw_id == other.properties["id"] - except Exception: # pylint: disable=broad-except - return False - - -class PhoneNumberProperties(TypedDict): - """Dictionary of properties for a PhoneNumberIdentifier.""" - - value: str - """The phone number in E.164 format.""" - asserted_id: NotRequired[str] - """The asserted Id set on a phone number to distinguish from other connections made through the same number.""" - is_anonymous: NotRequired[bool] - """True if the phone number is anonymous, e.g. when used to represent a hidden caller Id.""" - - -class PhoneNumberIdentifier: - """Represents a phone number.""" - - kind: Literal[CommunicationIdentifierKind.PHONE_NUMBER] = CommunicationIdentifierKind.PHONE_NUMBER - """The type of identifier.""" - properties: PhoneNumberProperties - """The properties of the identifier.""" - raw_id: str - """The raw ID of the identifier.""" - - def __init__(self, value: str, **kwargs: Any) -> None: - """ - :param str value: The phone number. - :keyword str raw_id: The raw ID of the identifier. If not specified, this will be constructed from - the 'value' parameter. - """ - - raw_id: Optional[str] = kwargs.get("raw_id") - is_anonymous: bool - - if raw_id is not None: - phone_number = raw_id[len(PHONE_NUMBER_PREFIX) :] - is_anonymous = phone_number == PHONE_NUMBER_ANONYMOUS_SUFFIX - asserted_id_index = -1 if is_anonymous else phone_number.rfind("_") + 1 - has_asserted_id = 0 < asserted_id_index < len(phone_number) - if has_asserted_id: - self.properties = PhoneNumberProperties( - value=value, is_anonymous=is_anonymous, asserted_id=phone_number[asserted_id_index:] - ) - else: - self.properties = PhoneNumberProperties(value=value, is_anonymous=is_anonymous) - else: - self.properties = PhoneNumberProperties(value=value) - self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) - - def __eq__(self, other): - try: - if other.raw_id: - return self.raw_id == other.raw_id - return self.raw_id == self._format_raw_id(other.properties) - except Exception: # pylint:disable=broad-except - return False - - def _format_raw_id(self, properties: PhoneNumberProperties) -> str: - # We just assume correct E.164 format here because - # validation should only happen server-side, not client-side. - value = properties["value"] - return f"{PHONE_NUMBER_PREFIX}{value}" - - -class UnknownIdentifier: - """Represents an identifier of an unknown type. - - It will be encountered in communications with endpoints that are not - identifiable by this version of the SDK. - - For checking yet unknown identifiers it is better to rely on the presence of the `raw_id` property, - as new or existing distinct type identifiers always contain the `raw_id` property. - It is not advisable to rely on the `kind` property with a value `unknown`, - as it could become a new or existing distinct type in the future. - """ - - kind: Literal[CommunicationIdentifierKind.UNKNOWN] = CommunicationIdentifierKind.UNKNOWN - """The type of identifier.""" - properties: Mapping[str, Any] - """The properties of the identifier.""" - raw_id: str - """The raw ID of the identifier.""" - - def __init__(self, identifier: str) -> None: - """ - :param str identifier: The ID of the identifier. - """ - self.raw_id = identifier - self.properties = {} - - def __eq__(self, other): - try: - return self.raw_id == other.raw_id - except AttributeError: - return False - - -class MicrosoftTeamsUserProperties(TypedDict): - """Dictionary of properties for a MicrosoftTeamsUserIdentifier.""" - - user_id: str - """The id of the Microsoft Teams user. If the user isn't anonymous, the id is the AAD object id of the user.""" - is_anonymous: bool - """Set this to true if the user is anonymous for example when joining a meeting with a share link.""" - cloud: Union[CommunicationCloudEnvironment, str] - """Cloud environment that this identifier belongs to.""" - - -class MicrosoftTeamsUserIdentifier: - """Represents an identifier for a Microsoft Teams user.""" - - kind: Literal[CommunicationIdentifierKind.MICROSOFT_TEAMS_USER] = CommunicationIdentifierKind.MICROSOFT_TEAMS_USER - """The type of identifier.""" - properties: MicrosoftTeamsUserProperties - """The properties of the identifier.""" - raw_id: str - """The raw ID of the identifier.""" - - def __init__(self, user_id: str, **kwargs: Any) -> None: - """ - :param str user_id: Microsoft Teams user id. - :keyword bool is_anonymous: `True` if the identifier is anonymous. Default value is `False`. - :keyword cloud: Cloud environment that the user belongs to. Default value is `PUBLIC`. - :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` - :keyword str raw_id: The raw ID of the identifier. If not specified, this value will be constructed from - the other properties. - """ - self.properties = MicrosoftTeamsUserProperties( - user_id=user_id, - is_anonymous=kwargs.get("is_anonymous", False), - cloud=kwargs.get("cloud") or CommunicationCloudEnvironment.PUBLIC, - ) - raw_id: Optional[str] = kwargs.get("raw_id") - self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) - - def __eq__(self, other): - try: - if other.raw_id: - return self.raw_id == other.raw_id - return self.raw_id == self._format_raw_id(other.properties) - except Exception: # pylint: disable=broad-except - return False - - def _format_raw_id(self, properties: MicrosoftTeamsUserProperties) -> str: - user_id = properties["user_id"] - if properties["is_anonymous"]: - return f"{TEAMS_USER_ANONYMOUS_PREFIX}{user_id}" - cloud = properties["cloud"] - if cloud == CommunicationCloudEnvironment.DOD: - return f"{TEAMS_USER_DOD_CLOUD_PREFIX}{user_id}" - if cloud == CommunicationCloudEnvironment.GCCH: - return f"{TEAMS_USER_GCCH_CLOUD_PREFIX}{user_id}" - if cloud == CommunicationCloudEnvironment.PUBLIC: - return f"{TEAMS_USER_PUBLIC_CLOUD_PREFIX}{user_id}" - return f"{TEAMS_USER_PUBLIC_CLOUD_PREFIX}{user_id}" - - -class MicrosoftTeamsAppProperties(TypedDict): - """Dictionary of properties for a MicrosoftTeamsAppIdentifier.""" - - app_id: str - """The id of the Microsoft Teams application.""" - cloud: Union[CommunicationCloudEnvironment, str] - """Cloud environment that this identifier belongs to.""" - - -class _botbackcompatdict(dict): - """Backwards compatible properties.""" - - def __getitem__(self, __key: Any) -> Any: - try: - return super().__getitem__(__key) - except KeyError: - if __key == "bot_id": - return super().__getitem__("app_id") - if __key == "is_resource_account_configured": - return True - raise - - -class MicrosoftTeamsAppIdentifier: - """Represents an identifier for a Microsoft Teams application.""" - - kind: Literal[CommunicationIdentifierKind.MICROSOFT_TEAMS_APP] = CommunicationIdentifierKind.MICROSOFT_TEAMS_APP - """The type of identifier.""" - properties: MicrosoftTeamsAppProperties - """The properties of the identifier.""" - raw_id: str - """The raw ID of the identifier.""" - - def __init__(self, app_id: str, **kwargs: Any) -> None: - """ - :param str app_id: Microsoft Teams application id. - :keyword cloud: Cloud environment that the application belongs to. Default value is `PUBLIC`. - :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` - :keyword str raw_id: The raw ID of the identifier. If not specified, this value will be constructed - from the other properties. - """ - self.properties = cast( - MicrosoftTeamsAppProperties, - _botbackcompatdict( - app_id=app_id, - cloud=kwargs.get("cloud") or CommunicationCloudEnvironment.PUBLIC, - ), - ) - raw_id: Optional[str] = kwargs.get("raw_id") - self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) - - def __eq__(self, other): - try: - if other.raw_id: - return self.raw_id == other.raw_id - return self.raw_id == self._format_raw_id(other.properties) - except Exception: # pylint: disable=broad-except - return False - - def _format_raw_id(self, properties: MicrosoftTeamsAppProperties) -> str: - app_id = properties["app_id"] - cloud = properties["cloud"] - if cloud == CommunicationCloudEnvironment.DOD: - return f"{TEAMS_APP_DOD_CLOUD_PREFIX}{app_id}" - if cloud == CommunicationCloudEnvironment.GCCH: - return f"{TEAMS_APP_GCCH_CLOUD_PREFIX}{app_id}" - return f"{TEAMS_APP_PUBLIC_CLOUD_PREFIX}{app_id}" - - -class _MicrosoftBotIdentifier(MicrosoftTeamsAppIdentifier): - """Represents an identifier for a Microsoft bot. - - DEPRECATED. Only used in cases of backwards compatibility. - """ - - def __init__(self, bot_id, **kwargs): - """ - :param str bot_id: Microsoft bot id. - :keyword bool is_resource_account_configured: `False` if the identifier is global. - Default value is `True` for tennantzed bots. - :keyword cloud: Cloud environment that the bot belongs to. Default value is `PUBLIC`. - :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` - """ - warnings.warn( - "The MicrosoftBotIdentifier is deprecated and has been replaced by MicrosoftTeamsAppIdentifier.", - DeprecationWarning, - ) - super().__init__(bot_id, **kwargs) - - -class TeamsExtensionUserProperties(TypedDict): - """Dictionary of properties for a TeamsExtensionUserIdentifier.""" - - user_id: str - """The id of the Teams extension user.""" - tenant_id: str - """The tenant id associated with the user.""" - resource_id: str - """The Communication Services resource id.""" - cloud: Union[CommunicationCloudEnvironment, str] - """Cloud environment that this identifier belongs to.""" - - -class TeamsExtensionUserIdentifier: - """Represents an identifier for a Teams Extension user.""" - - kind: Literal[CommunicationIdentifierKind.TEAMS_EXTENSION_USER] = CommunicationIdentifierKind.TEAMS_EXTENSION_USER - """The type of identifier.""" - properties: TeamsExtensionUserProperties - """The properties of the identifier.""" - raw_id: str - """The raw ID of the identifier.""" - - def __init__(self, *, user_id: str, tenant_id: str, resource_id: str, **kwargs: Any) -> None: - """ - :param str user_id: Teams extension user id. - :param str tenant_id: Tenant id associated with the user. - :param str resource_id: The Communication Services resource id. - :keyword cloud: Cloud environment that the user belongs to. Default value is `PUBLIC`. - :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` - :keyword str raw_id: The raw ID of the identifier. - If not specified, this value will be constructed from the other properties. - """ - self.properties = TeamsExtensionUserProperties( - user_id=user_id, - tenant_id=tenant_id, - resource_id=resource_id, - cloud=kwargs.get("cloud") or CommunicationCloudEnvironment.PUBLIC, - ) - raw_id: Optional[str] = kwargs.get("raw_id") - self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) - - def __eq__(self, other): - try: - if other.raw_id: - return self.raw_id == other.raw_id - return self.raw_id == self._format_raw_id(other.properties) - except Exception: # pylint: disable=broad-except - return False - - def _format_raw_id(self, properties: TeamsExtensionUserProperties) -> str: - # The prefix depends on the cloud - cloud = properties["cloud"] - if cloud == CommunicationCloudEnvironment.DOD: - prefix = ACS_USER_DOD_CLOUD_PREFIX - elif cloud == CommunicationCloudEnvironment.GCCH: - prefix = ACS_USER_GCCH_CLOUD_PREFIX - else: - prefix = ACS_USER_PREFIX - return f"{prefix}{properties['resource_id']}_{properties['tenant_id']}_{properties['user_id']}" - - -def try_create_teams_extension_user(prefix: str, suffix: str) -> Optional[TeamsExtensionUserIdentifier]: - segments = suffix.split("_") - if len(segments) != 3: - return None - resource_id, tenant_id, user_id = segments - if prefix == ACS_USER_PREFIX: - cloud = CommunicationCloudEnvironment.PUBLIC - elif prefix == ACS_USER_DOD_CLOUD_PREFIX: - cloud = CommunicationCloudEnvironment.DOD - elif prefix == ACS_USER_GCCH_CLOUD_PREFIX: - cloud = CommunicationCloudEnvironment.GCCH - else: - raise ValueError("Invalid MRI") - return TeamsExtensionUserIdentifier(user_id=user_id, tenant_id=tenant_id, resource_id=resource_id, cloud=cloud) - - -def identifier_from_raw_id(raw_id: str) -> CommunicationIdentifier: # pylint: disable=too-many-return-statements - """ - Creates a CommunicationIdentifier from a given raw ID. - - When storing raw IDs use this function to restore the identifier that was encoded in the raw ID. - - :param str raw_id: A raw ID to construct the CommunicationIdentifier from. - :return: The CommunicationIdentifier parsed from the raw_id. - :rtype: :class:`~.CommunicationIdentifier` - """ - if raw_id.startswith(PHONE_NUMBER_PREFIX): - return PhoneNumberIdentifier(value=raw_id[len(PHONE_NUMBER_PREFIX) :], raw_id=raw_id) - - segments = raw_id.split(":", maxsplit=2) - if len(segments) < 3: - return UnknownIdentifier(identifier=raw_id) - - prefix = f"{segments[0]}:{segments[1]}:" - suffix = segments[2] - if prefix == TEAMS_USER_ANONYMOUS_PREFIX: - return MicrosoftTeamsUserIdentifier(user_id=suffix, is_anonymous=True, raw_id=raw_id) - if prefix == TEAMS_USER_PUBLIC_CLOUD_PREFIX: - return MicrosoftTeamsUserIdentifier( - user_id=suffix, - is_anonymous=False, - cloud=CommunicationCloudEnvironment.PUBLIC, - raw_id=raw_id, - ) - if prefix == TEAMS_USER_DOD_CLOUD_PREFIX: - return MicrosoftTeamsUserIdentifier( - user_id=suffix, - is_anonymous=False, - cloud=CommunicationCloudEnvironment.DOD, - raw_id=raw_id, - ) - if prefix == TEAMS_USER_GCCH_CLOUD_PREFIX: - return MicrosoftTeamsUserIdentifier( - user_id=suffix, - is_anonymous=False, - cloud=CommunicationCloudEnvironment.GCCH, - raw_id=raw_id, - ) - if prefix == TEAMS_APP_PUBLIC_CLOUD_PREFIX: - return MicrosoftTeamsAppIdentifier( - app_id=suffix, - cloud=CommunicationCloudEnvironment.PUBLIC, - raw_id=raw_id, - ) - if prefix == TEAMS_APP_DOD_CLOUD_PREFIX: - return MicrosoftTeamsAppIdentifier( - app_id=suffix, - cloud=CommunicationCloudEnvironment.DOD, - raw_id=raw_id, - ) - if prefix == TEAMS_APP_GCCH_CLOUD_PREFIX: - return MicrosoftTeamsAppIdentifier( - app_id=suffix, - cloud=CommunicationCloudEnvironment.GCCH, - raw_id=raw_id, - ) - if prefix == SPOOL_USER_PREFIX: - return CommunicationUserIdentifier(id=raw_id, raw_id=raw_id) - - if prefix in [ - ACS_USER_PREFIX, - ACS_USER_DOD_CLOUD_PREFIX, - ACS_USER_GCCH_CLOUD_PREFIX, - ]: - identifier = try_create_teams_extension_user(prefix, suffix) - if identifier is not None: - return identifier - return CommunicationUserIdentifier(id=raw_id, raw_id=raw_id) - return UnknownIdentifier(identifier=raw_id) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py deleted file mode 100644 index 6eae18574193..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py +++ /dev/null @@ -1,118 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - -import hashlib -import base64 -import hmac -from urllib.parse import urlparse, unquote -from typing import Union - -from azure.core.credentials import AzureKeyCredential -from azure.core.pipeline.policies import SansIOHTTPPolicy -from azure.core.pipeline import PipelineRequest - -from .utils import get_current_utc_time - - -class HMACCredentialsPolicy(SansIOHTTPPolicy): - """Implementation of HMAC authentication policy. - - :param str host: The host of the endpoint url for Azure Communication Service resource - :param access_key: The access key we use to authenticate to the service - :type access_key: str or AzureKeyCredential - :param bool decode_url: `True` if there is a need to decode the url. Default value is `False` - """ - - def __init__( - self, - host: str, - access_key: Union[str, AzureKeyCredential], - decode_url: bool = False, - ) -> None: - super(HMACCredentialsPolicy, self).__init__() - - if host.startswith("https://"): - self._host = host.replace("https://", "") - - if host.startswith("http://"): - self._host = host.replace("http://", "") - - self._access_key = access_key - self._decode_url = decode_url - - def _compute_hmac(self, value: str) -> str: - if isinstance(self._access_key, AzureKeyCredential): - decoded_secret = base64.b64decode(self._access_key.key) - else: - decoded_secret = base64.b64decode(self._access_key) - - digest = hmac.new(decoded_secret, value.encode("utf-8"), hashlib.sha256).digest() - - return base64.b64encode(digest).decode("utf-8") - - def _sign_request(self, request: PipelineRequest) -> None: - verb = request.http_request.method.upper() - - # Get the path and query from url, which looks like https://host/path/query - parsed_url = urlparse(request.http_request.url) - query_url = parsed_url.path - - if parsed_url.query: - query_url += "?" + parsed_url.query - - # Need URL() to get a correct encoded key value, from "%3A" to ":", when transport is in type AioHttpTransport. - # There's a similar scenario in azure-storage-blob and azure-appconfiguration, the check logic is from there. - try: - from yarl import URL - from azure.core.pipeline.transport import ( # pylint:disable=non-abstract-transport-import - AioHttpTransport, - ) - - if ( - isinstance(request.context.transport, AioHttpTransport) - or isinstance( - getattr(request.context.transport, "_transport", None), - AioHttpTransport, - ) - or isinstance( - getattr( - getattr(request.context.transport, "_transport", None), - "_transport", - None, - ), - AioHttpTransport, - ) - ): - query_url = str(URL(query_url)) - except (ImportError, TypeError): - pass - - if self._decode_url: - query_url = unquote(query_url) - - signed_headers = "x-ms-date;host;x-ms-content-sha256" - - utc_now = get_current_utc_time() - if request.http_request.body is None: - request.http_request.body = "" - content_digest = hashlib.sha256((request.http_request.body.encode("utf-8"))).digest() - content_hash = base64.b64encode(content_digest).decode("utf-8") - - string_to_sign = verb + "\n" + query_url + "\n" + utc_now + ";" + self._host + ";" + content_hash - - signature = self._compute_hmac(string_to_sign) - - signature_header = { - "x-ms-date": utc_now, - "x-ms-content-sha256": content_hash, - "x-ms-return-client-request-id": "true", - "Authorization": "HMAC-SHA256 SignedHeaders=" + signed_headers + "&Signature=" + signature, - } - - request.http_request.headers.update(signature_header) - - def on_request(self, request: PipelineRequest) -> None: - self._sign_request(request) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py deleted file mode 100644 index 1b0dde140479..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py +++ /dev/null @@ -1,78 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -import json -from typing import Any, List, Optional - -# pylint: disable=non-abstract-transport-import -# pylint: disable=no-name-in-module - -from azure.core.credentials import AccessToken -from azure.core.pipeline import Pipeline, PipelineResponse -from azure.core.pipeline.policies import BearerTokenCredentialPolicy -from azure.core.exceptions import HttpResponseError -from azure.core.credentials import TokenCredential -from azure.core.pipeline.policies import RetryPolicy -from .entra_token_guard_policy import EntraTokenGuardPolicy -from . import token_utils - - -class TokenExchangeClient: - """Represents a client that exchanges an Entra token for an Azure Communication Services (ACS) token. - - :param resource_endpoint: The endpoint URL of the resource to authenticate against. - :param credential: The credential to use for token exchange. - :param scopes: The scopes to request during the token exchange. - :keyword transport: Optional transport to use for the pipeline. - """ - - # pylint: disable=C4748 - # pylint: disable=client-method-missing-type-annotations - def __init__( - self, resource_endpoint: str, credential: TokenCredential, scopes: Optional[List[str]] = None, **kwargs: Any - ): - - self._resource_endpoint = resource_endpoint - self._scopes = scopes or ["https://communication.azure.com/clients/.default"] - self._credential = credential - pipeline_transport = kwargs.get("transport", None) - self._pipeline = self._create_pipeline_from_options(pipeline_transport) - - def _create_pipeline_from_options(self, pipeline_transport): - auth_policy = BearerTokenCredentialPolicy(self._credential, *self._scopes) - entra_token_guard_policy = EntraTokenGuardPolicy() - retry_policy = RetryPolicy() - policies = [auth_policy, entra_token_guard_policy, retry_policy] - if pipeline_transport: - return Pipeline(policies=policies, transport=pipeline_transport) - - from azure.core.pipeline.transport import RequestsTransport - - return Pipeline(policies=policies, transport=RequestsTransport()) - - def exchange_entra_token(self) -> AccessToken: - message = token_utils.create_request_message(self._resource_endpoint, self._scopes) - response = self._pipeline.run(message) - return self._parse_access_token_from_response(response) - - def _parse_access_token_from_response(self, response: PipelineResponse) -> AccessToken: - if response.http_response.status_code == 200: - try: - content = response.http_response.text() - data = json.loads(content) - access_token_json = data["accessToken"] - token = access_token_json["token"] - expires_on = access_token_json["expiresOn"] - expires_on_epoch = token_utils.parse_expires_on(expires_on, response) - if expires_on_epoch is None: - raise ValueError("Failed to parse 'expiresOn' value from access token response") - return AccessToken(token, expires_on_epoch) - except Exception as ex: - raise ValueError("Failed to parse access token from response") from ex - else: - raise HttpResponseError( - message="Failed to exchange Entra token for ACS token", response=response.http_response - ) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py deleted file mode 100644 index 22dd4582460b..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py +++ /dev/null @@ -1,82 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -import json -from typing import Any, Optional, List - -# pylint: disable=non-abstract-transport-import -# pylint: disable=no-name-in-module - -from azure.core.credentials import AccessToken -from azure.core.pipeline import AsyncPipeline, PipelineResponse -from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from azure.core.exceptions import HttpResponseError -from azure.core.credentials_async import AsyncTokenCredential -from azure.core.pipeline.policies import AsyncRetryPolicy -from .entra_token_guard_policy_async import EntraTokenGuardPolicy -from . import token_utils - - -class TokenExchangeClient: - """Asynchronous client that exchanges an Entra token for an Azure Communication Services (ACS) token. - - :param resource_endpoint: The endpoint URL of the resource to authenticate against. - :param credential: The credential to use for token exchange. - :param scopes: The scopes to request during the token exchange. - :keyword transport: Optional transport to use for the pipeline. - """ - - # pylint: disable=C4748 - # pylint: disable=client-method-missing-type-annotations - def __init__( - self, - resource_endpoint: str, - credential: AsyncTokenCredential, - scopes: Optional[List[str]] = None, - **kwargs: Any - ): - - self._resource_endpoint = resource_endpoint - self._scopes = scopes or ["https://communication.azure.com/clients/.default"] - self._credential = credential - pipeline_transport = kwargs.get("transport", None) - self._pipeline = self._create_pipeline_from_options(pipeline_transport) - - def _create_pipeline_from_options(self, pipeline_transport): - auth_policy = AsyncBearerTokenCredentialPolicy(self._credential, *self._scopes) - entra_token_guard_policy = EntraTokenGuardPolicy() - retry_policy = AsyncRetryPolicy() - policies = [auth_policy, entra_token_guard_policy, retry_policy] - if pipeline_transport: - return AsyncPipeline(policies=policies, transport=pipeline_transport) - - from azure.core.pipeline.transport import AioHttpTransport - - return AsyncPipeline(policies=policies, transport=AioHttpTransport()) - - async def exchange_entra_token(self) -> AccessToken: - message = token_utils.create_request_message(self._resource_endpoint, self._scopes) - response = await self._pipeline.run(message) - return await self._parse_access_token_from_response(response) - - async def _parse_access_token_from_response(self, response: PipelineResponse) -> AccessToken: - if response.http_response.status_code == 200: - try: - content = response.http_response.text() - data = json.loads(content) - access_token_json = data["accessToken"] - token = access_token_json["token"] - expires_on = access_token_json["expiresOn"] - expires_on_epoch = token_utils.parse_expires_on(expires_on, response) - if expires_on_epoch is None: - raise ValueError("Failed to parse 'expiresOn' value from access token response") - return AccessToken(token, expires_on_epoch) - except Exception as ex: - raise ValueError("Failed to parse access token from response") from ex - else: - raise HttpResponseError( - message="Failed to exchange Entra token for ACS token", response=response.http_response - ) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py deleted file mode 100644 index 9628d3e76ec9..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py +++ /dev/null @@ -1,91 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - - -import json -from datetime import datetime, timezone -from typing import Tuple, Any, List, Optional -import isodate -from azure.core.exceptions import HttpResponseError - -TEAMS_EXTENSION_SCOPE_PREFIX = "https://auth.msft.communication.azure.com/" -COMMUNICATION_CLIENTS_SCOPE_PREFIX = "https://communication.azure.com/clients/" -TEAMS_EXTENSION_ENDPOINT = "/access/teamsExtension/:exchangeAccessToken" -TEAMS_EXTENSION_API_VERSION = "2025-06-30" -COMMUNICATION_CLIENTS_ENDPOINT = "/access/entra/:exchangeAccessToken" -COMMUNICATION_CLIENTS_API_VERSION = "2025-03-02-preview" - - -def create_request_message(resource_endpoint: str, scopes: Optional[List[str]]) -> Any: - from azure.core.pipeline.transport import HttpRequest - - request_uri = create_request_uri(resource_endpoint, scopes) - request = HttpRequest("POST", request_uri) - request.headers["Accept"] = "application/json" - request.headers["Content-Type"] = "application/json" - request.set_json_body({}) - return request - - -def create_request_uri(resource_endpoint: str, scopes: Optional[List[str]]) -> str: - endpoint, api_version = determine_endpoint_and_api_version(scopes) - base = resource_endpoint.rstrip("/") - return f"{base}{endpoint}?api-version={api_version}" - - -def determine_endpoint_and_api_version(scopes: Optional[List[str]]) -> Tuple[str, str]: - if not scopes or not isinstance(scopes, list): - raise ValueError( - "Scopes validation failed. Ensure all scopes start with either " - f"{TEAMS_EXTENSION_SCOPE_PREFIX} or {COMMUNICATION_CLIENTS_SCOPE_PREFIX}." - ) - if all(scope.startswith(TEAMS_EXTENSION_SCOPE_PREFIX) for scope in scopes): - return TEAMS_EXTENSION_ENDPOINT, TEAMS_EXTENSION_API_VERSION - if all(scope.startswith(COMMUNICATION_CLIENTS_SCOPE_PREFIX) for scope in scopes): - return COMMUNICATION_CLIENTS_ENDPOINT, COMMUNICATION_CLIENTS_API_VERSION - raise ValueError( - "Scopes validation failed. Ensure all scopes start with either" - f"{TEAMS_EXTENSION_SCOPE_PREFIX} or {COMMUNICATION_CLIENTS_SCOPE_PREFIX}." - ) - - -def parse_expires_on(expires_on, response): - if isinstance(expires_on, str): - try: - expires_on_dt = isodate.parse_datetime(expires_on) - expires_on_epoch = int(expires_on_dt.timestamp()) - return expires_on_epoch - except Exception as exc: - raise HttpResponseError( - message="Unknown format for expires_on field in access token response", response=response.http_response - ) from exc - else: - raise HttpResponseError( - message="Missing expires_on field in access token response", response=response.http_response - ) - - -def is_entra_token_cache_valid(entra_token_cache, request): - current_entra_token = request.http_request.headers.get("Authorization", "") - cache_valid = entra_token_cache is not None and current_entra_token == entra_token_cache - return cache_valid, current_entra_token - - -def is_acs_token_cache_valid(response_cache): - if ( - response_cache is None - or response_cache.http_response is None - or response_cache.http_response.status_code != 200 - ): - return False - try: - content = response_cache.http_response.text() - data = json.loads(content) - expires_on = data["accessToken"]["expiresOn"] - expires_on_dt = isodate.parse_datetime(expires_on) - return datetime.now(timezone.utc) < expires_on_dt - except (KeyError, ValueError, json.JSONDecodeError) as e: - raise ValueError("Invalid token response") from e diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py deleted file mode 100644 index 17adbf77dcc9..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ /dev/null @@ -1,213 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event -from datetime import timedelta -from typing import Any, Optional, overload, Callable -from azure.core.credentials import TokenCredential, AccessToken -from .utils import get_current_utc_as_int -from .utils import create_access_token -from .token_exchange import TokenExchangeClient - - -class CommunicationTokenCredential(object): - """Credential type used for authenticating to an Azure Communication service. - - :param str token: The token used to authenticate to an Azure Communication service. - :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). - :paramtype token_refresher: Callable[[], AccessToken] - :keyword bool proactive_refresh: Whether to refresh the token proactively or not. - If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use - a background thread to attempt to refresh the token within 10 minutes before the cached token expires. - The proactive refresh will request a new token by calling the 'token_refresher' callback. - When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager - or the 'close' method must be called once the object usage has been finished. - :keyword str resource_endpoint: The endpoint URL of the resource to authenticate against. - :keyword token_credential: The credential to use for token exchange. - :paramtype token_credential: ~azure.core.credentials.TokenCredential - :keyword list[str] scopes: The scopes to request during the token exchange. If not provided, - a default value will be used: https://communication.azure.com/clients/.default - - :raises: TypeError if parameter 'token' is not a string - :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. - """ - - _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - - @overload - def __init__( - self, - token: str, - *, - token_refresher: Optional[Callable[[], AccessToken]] = None, - proactive_refresh: bool = False, - **kwargs: Any, - ): - """ - Initializes the CommunicationTokenCredential. - - :param str token: The token used to authenticate to an Azure Communication service. - :param token_refresher: Optional callable to refresh the token. - :param proactive_refresh: Whether to refresh the token proactively. - :param kwargs: Additional keyword arguments. - """ - - @overload - def __init__( - self, - *, - resource_endpoint: str, - token_credential: TokenCredential, - scopes: Optional[list[str]] = None, - **kwargs: Any, - ): - """ - Initializes the CommunicationTokenCredential using token exchange. - - :param resource_endpoint: The endpoint URL of the resource to authenticate against. - :param token_credential: The credential to use for token exchange. - :param scopes: The scopes to request during the token exchange. - :param kwargs: Additional keyword arguments. - """ - - def __init__(self, token: Optional[str] = None, **kwargs: Any): - resource_endpoint = kwargs.pop("resource_endpoint", None) - token_credential = kwargs.pop("token_credential", None) - scopes = kwargs.pop("scopes", None) - - # Check if at least one field exists but not all fields exist when token is None - fields_present = [resource_endpoint, token_credential] - fields_exist = [field is not None for field in fields_present] - - if token is None and not all(fields_exist): - missing_fields = [] - if resource_endpoint is None: - missing_fields.append("resource_endpoint") - if token_credential is None: - missing_fields.append("token_credential") - raise ValueError( - "When using token exchange, resource_endpoint and token_credential must be provided. " - f"Missing: {', '.join(missing_fields)}" - ) - - self._token_exchange_client = None - if resource_endpoint and token_credential: - self._token_exchange_client = TokenExchangeClient(resource_endpoint, token_credential, scopes) - self._token_refresher = self._token_exchange_client.exchange_entra_token - self._proactive_refresh = False - self._token = self._token_exchange_client.exchange_entra_token() - else: - if not isinstance(token, str): - raise TypeError("Token must be a string.") - self._token = create_access_token(token) - self._token_refresher = kwargs.pop("token_refresher", None) - self._proactive_refresh = kwargs.pop("proactive_refresh", False) - if self._proactive_refresh and self._token_refresher is None: - raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") - self._timer = None - self._lock = Condition(Lock()) - self._some_thread_refreshing = False - self._is_closed = Event() - - def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken - """The value of the configured token. - :param any scopes: Scopes to be added to the token. - :return: AccessToken - :rtype: ~azure.core.credentials.AccessToken - """ - if self._proactive_refresh and self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") - - if not self._token_refresher or not self._is_token_expiring_soon(self._token): - return self._token - self._update_token_and_reschedule() - return self._token - - def _update_token_and_reschedule(self): - should_this_thread_refresh = False - with self._lock: - while self._is_token_expiring_soon(self._token): - if self._some_thread_refreshing: - if self._is_token_valid(self._token): - return self._token - self._wait_till_lock_owner_finishes_refreshing() - else: - should_this_thread_refresh = True - self._some_thread_refreshing = True - break - - if should_this_thread_refresh: - try: - new_token = self._token_refresher() - if not self._is_token_valid(new_token): - raise ValueError("The token returned from the token_refresher is expired.") - with self._lock: - self._token = new_token - self._some_thread_refreshing = False - self._lock.notify_all() - except: - with self._lock: - self._some_thread_refreshing = False - self._lock.notify_all() - raise - if self._proactive_refresh: - self._schedule_refresh() - return self._token - - def _schedule_refresh(self): - if self._is_closed.is_set(): - return - if self._timer is not None: - self._timer.cancel() - - token_ttl = self._token.expires_on - get_current_utc_as_int() - - if self._is_token_expiring_soon(self._token): - # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl // 2 - else: - # Schedule the next refresh for when it gets in to the soon-to-expire window. - timespan = token_ttl - timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - if timespan <= TIMEOUT_MAX: - self._timer = Timer(timespan, self._update_token_and_reschedule) - self._timer.daemon = True - self._timer.start() - - def _wait_till_lock_owner_finishes_refreshing(self): - self._lock.release() - self._lock.acquire() - - def _is_token_expiring_soon(self, token): - if self._proactive_refresh: - interval = timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) - else: - interval = timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return (token.expires_on - get_current_utc_as_int()) < interval.total_seconds() - - @classmethod - def _is_token_valid(cls, token): - return get_current_utc_as_int() < token.expires_on - - def __enter__(self): - if self._proactive_refresh: - if self._is_closed.is_set(): - raise RuntimeError( - "An instance of CommunicationTokenCredential cannot be reused once it has been closed." - ) - self._schedule_refresh() - return self - - def __exit__(self, *args): - self.close() - - def close(self) -> None: - if self._timer is not None: - self._timer.cancel() - self._timer = None - self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py deleted file mode 100644 index 81c4b79c4e6d..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ /dev/null @@ -1,225 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -# pylint: disable=C4763 - -import asyncio -import sys -from asyncio import Condition, Lock, Event -from datetime import timedelta -from typing import Any, Optional, overload, Awaitable, Callable -from azure.core.credentials_async import AsyncTokenCredential -from .utils import get_current_utc_as_int -from .utils import create_access_token -from .utils_async import AsyncTimer -from .token_exchange_async import TokenExchangeClient - - -class CommunicationTokenCredential(object): - """Credential type used for authenticating to an Azure Communication service. - - :param str token: The token used to authenticate to an Azure Communication service. - :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). - :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] - :keyword bool proactive_refresh: Whether to refresh the token proactively or not. - If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use - a background thread to attempt to refresh the token within 10 minutes before the cached token - expires. The proactive refresh will request a new token by calling the 'token_refresher' callback. - When 'proactive_refresh' is enabled, the Credential object must be either run within a context - manager or the 'close' method must be called once the object usage has been finished. - :keyword str resource_endpoint: The endpoint URL of the resource to authenticate against. - :keyword token_credential: The credential to use for token exchange. - :paramtype token_credential: ~azure.core.credentials.AsyncTokenCredential - :keyword list[str] scopes: The scopes to request during the token exchange. If not provided, - a default value will be used: https://communication.azure.com/clients/.default - - :raises: TypeError if parameter 'token' is not a string - :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. - """ - - _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - - @overload - def __init__( - self, - token: str, - *, - token_refresher: Optional[Callable[[], Awaitable[Any]]] = None, - proactive_refresh: bool = False, - **kwargs: Any, - ): - """ - Initializes the CommunicationTokenCredential. - - :param str token: The token used to authenticate to an Azure Communication service. - :param token_refresher: Optional async callable to refresh the token. - :param proactive_refresh: Whether to refresh the token proactively. - :param kwargs: Additional keyword arguments. - """ - - @overload - def __init__( - self, - *, - resource_endpoint: str, - token_credential: AsyncTokenCredential, - scopes: Optional[list[str]] = None, - **kwargs: Any, - ): - """ - Initializes the CommunicationTokenCredential using token exchange. - - :param resource_endpoint: The endpoint URL of the resource to authenticate against. - :param token_credential: The credential to use for token exchange. - :param scopes: The scopes to request during the token exchange. - :param kwargs: Additional keyword arguments. - """ - - def __init__(self, token: Optional[str] = None, **kwargs: Any): - resource_endpoint = kwargs.pop("resource_endpoint", None) - token_credential = kwargs.pop("token_credential", None) - scopes = kwargs.pop("scopes", None) - - # Check if at least one field exists but not all fields exist when token is None - fields_present = [resource_endpoint, token_credential] - fields_exist = [field is not None for field in fields_present] - - if token is None and not all(fields_exist): - missing_fields = [] - if resource_endpoint is None: - missing_fields.append("resource_endpoint") - if token_credential is None: - missing_fields.append("token_credential") - raise ValueError( - "When using token exchange, resource_endpoint and token_credential must be provided. " - f"Missing: {', '.join(missing_fields)}" - ) - - self._token_exchange_client = None - if resource_endpoint and token_credential: - self._token_exchange_client = TokenExchangeClient(resource_endpoint, token_credential, scopes) - self._token_refresher = self._token_exchange_client.exchange_entra_token - self._proactive_refresh = False - - async def _initialize_token(): - self._token = await self._token_exchange_client.exchange_entra_token() - - asyncio.get_event_loop().run_until_complete(_initialize_token()) - else: - if not isinstance(token, str): - raise TypeError("Token must be a string.") - self._token = create_access_token(token) - self._token_refresher = kwargs.pop("token_refresher", None) - self._proactive_refresh = kwargs.pop("proactive_refresh", False) - if self._proactive_refresh and self._token_refresher is None: - raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") - - self._timer = None - self._async_mutex = Lock() - if sys.version_info[:3] == (3, 10, 0): - # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): - getattr(self._async_mutex, "_get_loop", lambda: None)() - self._lock = Condition(self._async_mutex) - self._some_thread_refreshing = False - self._is_closed = Event() - - async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken - """The value of the configured token. - :param any scopes: Scopes to be added to the token. - :return: AccessToken - :rtype: ~azure.core.credentials.AccessToken - """ - if self._proactive_refresh and self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") - - if not self._token_refresher or not self._is_token_expiring_soon(self._token): - return self._token - await self._update_token_and_reschedule() - return self._token - - async def _update_token_and_reschedule(self): - should_this_thread_refresh = False - async with self._lock: - while self._is_token_expiring_soon(self._token): - if self._some_thread_refreshing: - if self._is_token_valid(self._token): - return self._token - await self._wait_till_lock_owner_finishes_refreshing() - else: - should_this_thread_refresh = True - self._some_thread_refreshing = True - break - - if should_this_thread_refresh: - try: - new_token = await self._token_refresher() - if not self._is_token_valid(new_token): - raise ValueError("The token returned from the token_refresher is expired.") - async with self._lock: - self._token = new_token - self._some_thread_refreshing = False - self._lock.notify_all() - except: - async with self._lock: - self._some_thread_refreshing = False - self._lock.notify_all() - raise - if self._proactive_refresh: - self._schedule_refresh() - return self._token - - def _schedule_refresh(self): - if self._is_closed.is_set(): - return - if self._timer is not None: - self._timer.cancel() - - token_ttl = self._token.expires_on - get_current_utc_as_int() - - if self._is_token_expiring_soon(self._token): - # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl // 2 - else: - # Schedule the next refresh for when it gets in to the soon-to-expire window. - timespan = token_ttl - timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - - self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) - self._timer.start() - - async def _wait_till_lock_owner_finishes_refreshing(self): - self._lock.release() - await self._lock.acquire() - - def _is_token_expiring_soon(self, token): - if self._proactive_refresh: - interval = timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) - else: - interval = timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return (token.expires_on - get_current_utc_as_int()) < interval.total_seconds() - - @classmethod - def _is_token_valid(cls, token): - return get_current_utc_as_int() < token.expires_on - - async def __aenter__(self): - if self._proactive_refresh: - if self._is_closed.is_set(): - raise RuntimeError( - "An instance of CommunicationTokenCredential cannot be reused once it has been closed." - ) - self._schedule_refresh() - return self - - async def __aexit__(self, *args): - await self.close() - - async def close(self) -> None: - if self._timer is not None: - self._timer.cancel() - self._timer = None - self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py deleted file mode 100644 index 38a62a49dc3f..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ /dev/null @@ -1,89 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - -import base64 -import json -import calendar -from typing import cast, Tuple, Optional -from datetime import datetime -from azure.core.serialization import TZ_UTC -from azure.core.credentials import AccessToken - - -def _convert_datetime_to_utc_int(input_datetime) -> int: - """ - Converts DateTime in local time to the Epoch in UTC in second. - - :param input_datetime: Input datetime - :type input_datetime: datetime - :return: Integer - :rtype: int - """ - return int(calendar.timegm(input_datetime.utctimetuple())) - - -def parse_connection_str(conn_str: Optional[str]) -> Tuple[str, str]: - if conn_str is None: - raise ValueError("Connection string is undefined.") - endpoint = None - shared_access_key = None - for element in conn_str.split(";"): - key, _, value = element.partition("=") - if key.lower() == "endpoint": - endpoint = value.rstrip("/") - elif key.lower() == "accesskey": - shared_access_key = value - if not all([endpoint, shared_access_key]): - raise ValueError( - "Invalid connection string. You can get the connection string from your resource page in the Azure Portal. " - "The format should be as follows: endpoint=https:///;accesskey=" - ) - left_slash_pos = cast(str, endpoint).find("//") - if left_slash_pos != -1: - host = cast(str, endpoint)[left_slash_pos + 2 :] - else: - host = str(endpoint) - - return host, str(shared_access_key) - - -def get_current_utc_time() -> str: - return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" - - -def get_current_utc_as_int() -> int: - current_utc_datetime = datetime.utcnow() - return _convert_datetime_to_utc_int(current_utc_datetime) - - -def create_access_token(token) -> AccessToken: - """Creates an instance of azure.core.credentials.AccessToken from a - string token. The input string is jwt token in the following form: - .. - This method looks into the token_payload which is a json and extracts the expiry time - for that token and creates a tuple of type azure.core.credentials.AccessToken - (, ) - :param token: User token - :type token: str - :return: Instance of azure.core.credentials.AccessToken - token and expiry date of it - :rtype: ~azure.core.credentials.AccessToken - """ - - token_parse_err_msg = "Token is not formatted correctly" - parts = token.split(".") - - if len(parts) < 3: - raise ValueError(token_parse_err_msg) - - try: - padded_base64_payload = base64.b64decode(parts[1] + "==").decode("ascii") - payload = json.loads(padded_base64_payload) - return AccessToken( - token, - _convert_datetime_to_utc_int(datetime.fromtimestamp(payload["exp"], TZ_UTC)), - ) - except ValueError as val_error: - raise ValueError(token_parse_err_msg) from val_error diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py deleted file mode 100644 index b779d5c814b3..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py +++ /dev/null @@ -1,31 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- -# pylint: disable=C4763 -from asyncio import sleep, ensure_future - - -class AsyncTimer: - """A non-blocking timer, that calls a function after a specified number of seconds: - :param int interval: time interval in seconds - :param callable callback: function to be called after the interval has elapsed - """ - - def __init__(self, interval, callback): - self._interval = interval - self._callback = callback - self._task = None - - def start(self): - self._task = ensure_future(self._job()) - - async def _job(self): - await sleep(self._interval) - await self._callback() - - def cancel(self): - if self._task is not None: - self._task.cancel() - self._task = None diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py deleted file mode 100644 index 0d6042f38bcf..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py +++ /dev/null @@ -1,20 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - -from datetime import timedelta -from typing import Optional - - -def convert_timedelta_to_mins( - duration: Optional[timedelta], -) -> Optional[int]: - """ - Returns the total number of minutes contained in the duration. - :param duration: Time duration - :type duration: ~datetime.timedelta - :rtype: int - """ - return None if duration is None else int(duration.total_seconds() / 60) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py index fa4bba0a08c4..4e2942f574c4 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py @@ -1,9 +1,9 @@ -# ------------------------------------------------------------------------- +# coding=utf-8 +# -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- VERSION = "1.6.0" - -SDK_MONIKER = f"communication-identity/{VERSION}" # type: str diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py deleted file mode 100644 index 466f314d6673..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ /dev/null @@ -1,224 +0,0 @@ -# coding=utf-8 -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -from datetime import timedelta -from typing import List, Optional, Union, Tuple -from azure.core.tracing.decorator_async import distributed_trace_async -from azure.core.credentials import AccessToken -from azure.core.credentials_async import AsyncTokenCredential -from azure.core.credentials import AzureKeyCredential -from .._generated.aio._client import ( - CommunicationIdentityClient as CommunicationIdentityClientGen, -) -from .._shared.auth_policy_utils import get_authentication_policy -from .._shared.utils import parse_connection_str -from .._shared.models import CommunicationUserIdentifier -from .._version import SDK_MONIKER -from .._api_versions import DEFAULT_VERSION -from .._utils import convert_timedelta_to_mins -from .._generated.models import CommunicationTokenScope - - -class CommunicationIdentityClient: - """Azure Communication Services Identity client. - - :param str endpoint: - The endpoint url for Azure Communication Service resource. - :param Union[AsyncTokenCredential, AzureKeyCredential] credential: - The credential we use to authenticate against the service. - :keyword api_version: Azure Communication Identity API version. - Default value is "2022-06-01". Note that overriding this default value may result in unsupported behavior. - :paramtype api_version: str - - .. admonition:: Example: - - .. literalinclude:: ../samples/identity_samples_async.py - :language: python - """ - - def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureKeyCredential], **kwargs) -> None: - try: - if not endpoint.lower().startswith("http"): - endpoint = "https://" + endpoint - except AttributeError as err: - raise ValueError("Account URL must be a string.") from err - - if not credential: - raise ValueError("You need to provide account shared key to authenticate.") - - self._endpoint = endpoint - self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) - self._identity_service_client = CommunicationIdentityClientGen( - self._endpoint, - api_version=self._api_version, - authentication_policy=get_authentication_policy(endpoint, credential, decode_url=True, is_async=True), - sdk_moniker=SDK_MONIKER, - **kwargs - ) - - @classmethod - def from_connection_string(cls, conn_str: str, **kwargs) -> "CommunicationIdentityClient": - """Create CommunicationIdentityClient from a Connection String. - - :param str conn_str: - A connection string to an Azure Communication Service resource. - :returns: Instance of CommunicationIdentityClient. - :rtype: ~azure.communication.identity.aio.CommunicationIdentityClient - """ - endpoint, access_key = parse_connection_str(conn_str) - - # There is logic downstream in method `get_authentication_policy` to handle string credential. - # Marking this as type: ignore to resolve mypy warning. - return cls(endpoint, access_key, **kwargs) # type: ignore - - @distributed_trace_async - async def create_user(self, **kwargs) -> "CommunicationUserIdentifier": - """create a single Communication user - - :return: CommunicationUserIdentifier - :rtype: ~azure.communication.identity.CommunicationUserIdentifier - """ - identity_access_token = await self._identity_service_client.communication_identity.create(**kwargs) - - return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) - - @distributed_trace_async - async def create_user_and_token( - self, scopes: List[Union[str, CommunicationTokenScope]], *, - token_expires_in: Optional[timedelta] = None, - **kwargs - ) -> Tuple["CommunicationUserIdentifier", AccessToken]: - """Create a single Communication user with an identity token. - - :param scopes: - List of scopes to be added to the token. - :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] - :keyword token_expires_in: Custom validity period of the Communication Identity access token - within [1, 24] hours range. If not provided, the default value of 24 hours will be used. - :paramtype token_expires_in: ~datetime.timedelta - :return: A tuple of a CommunicationUserIdentifier and a AccessToken. - :rtype: - tuple of (~azure.communication.identity.CommunicationUserIdentifier, ~azure.core.credentials.AccessToken) - """ - request_body = { - "createTokenWithScopes": scopes, - "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), - } - - identity_access_token = await self._identity_service_client.communication_identity.create( - body=request_body, **kwargs # type: ignore - ) - - user_identifier = CommunicationUserIdentifier( - identity_access_token.identity.id, raw_id=identity_access_token.identity.id - ) - access_token = AccessToken( - identity_access_token.access_token.token, - identity_access_token.access_token.expires_on, - ) - - return user_identifier, access_token - - @distributed_trace_async - async def delete_user(self, user: CommunicationUserIdentifier, **kwargs) -> None: - """Triggers revocation event for user and deletes all its data. - - :param user: - Azure Communication User to delete - :type user: ~azure.communication.identity.CommunicationUserIdentifier - :return: None - :rtype: None - """ - await self._identity_service_client.communication_identity.delete(user.properties["id"], **kwargs) - - @distributed_trace_async - async def get_token( - self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], *, - token_expires_in: Optional[timedelta] = None, - **kwargs - ) -> AccessToken: - """Generates a new token for an identity. - - :param user: Azure Communication User - :type user: ~azure.communication.identity.CommunicationUserIdentifier - :param scopes: - List of scopes to be added to the token. - :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] - :keyword ~datetime.timedelta token_expires_in: Custom validity period of the Communication Identity access token - within [1, 24] hours range. If not provided, the default value of 24 hours will be used. - :return: AccessToken - :rtype: ~azure.core.credentials.AccessToken - """ - request_body = { - "scopes": scopes, - "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), - } - - access_token = await self._identity_service_client.communication_identity.issue_access_token( - user.properties["id"], body=request_body, **kwargs # type: ignore - ) - - return AccessToken(access_token.token, access_token.expires_on) - - @distributed_trace_async - async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> None: - """Schedule revocation of all tokens of an identity. - - :param user: Azure Communication User. - :type user: ~azure.communication.identity.CommunicationUserIdentifier - :return: None - :rtype: None - """ - return await self._identity_service_client.communication_identity.revoke_access_tokens( - user.properties["id"] if user else None, **kwargs # type: ignore - ) - - @distributed_trace_async - async def get_token_for_teams_user( - self, - aad_token, # type: str - client_id, # type: str - user_object_id, # type: str - **kwargs - ): - # type: (...) -> AccessToken - """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. - - :param aad_token: an Azure AD access token of a Teams User - :type aad_token: str - :param client_id: a Client ID of an Azure AD application to be verified against - the appId claim in the Azure AD access token. - :type client_id: str - :param user_object_id: an Object ID of an Azure AD user (Teams User) to be verified against - the OID claim in the Azure AD access token. - :type user_object_id: str - :return: AccessToken - :rtype: ~azure.core.credentials.AccessToken - """ - request_body = { - "token": aad_token, - "appId": client_id, - "userId": user_object_id, - } - access_token = await self._identity_service_client.communication_identity.exchange_teams_user_access_token( - body=request_body, **kwargs # type: ignore - ) - - return AccessToken(access_token.token, access_token.expires_on) - - async def __aenter__(self) -> "CommunicationIdentityClient": - await self._identity_service_client.__aenter__() - return self - - async def __aexit__(self, *args) -> None: - await self.close() - - async def close(self) -> None: - """Close the :class: - `~azure.communication.identity.aio.CommunicationIdentityClient` session. - """ - await self._identity_service_client.__aexit__() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed b/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed index e69de29bb2d1..e5aff4f83af8 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/pyproject.toml b/sdk/communication/azure-communication-identity/pyproject.toml index db898be6409d..5118e829b7ef 100644 --- a/sdk/communication/azure-communication-identity/pyproject.toml +++ b/sdk/communication/azure-communication-identity/pyproject.toml @@ -1,9 +1,83 @@ +[build-system] +requires = [ + "setuptools>=77.0.3", + "wheel", +] +build-backend = "setuptools.build_meta" + +[project] +name = "azure-communication-identity" +authors = [ + { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" }, +] +description = "Microsoft Corporation Azure Communication Identity Client Library for Python" +license = "MIT" +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +requires-python = ">=3.10" +keywords = [ + "azure", + "azure sdk", +] +dependencies = [ + "isodate>=0.6.1", + "azure-core>=1.37.0", + "typing-extensions>=4.6.0", +] +dynamic = [ + "version", + "readme", +] + +[project.urls] +repository = "https://github.com/Azure/azure-sdk-for-python" + +[tool.setuptools.dynamic.version] +attr = "azure.communication.identity._version.VERSION" + +[tool.setuptools.dynamic.readme] +file = [ + "README.md", + "CHANGELOG.md", +] +content-type = "text/markdown" + +[tool.setuptools.packages.find] +exclude = [ + "tests*", + "generated_tests*", + "samples*", + "generated_samples*", + "doc*", + "azure", + "azure.communication", +] + +[tool.setuptools.package-data] +pytyped = [ + "py.typed", +] + [tool.azure-sdk-build] pyright = false -[tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" - [tool.azure-sdk-conda] in_bundle = true bundle_name = "azure-communication" + +[packaging] +auto_update = false +package_name = "azure-communication-identity" +package_pprint_name = "Communication Identity Service" +package_doc_id = "" +is_stable = false +is_arm = false diff --git a/sdk/communication/azure-communication-identity/samples/identity_samples.py b/sdk/communication/azure-communication-identity/samples/identity_samples.py index 781afd26041c..3c380a935169 100644 --- a/sdk/communication/azure-communication-identity/samples/identity_samples.py +++ b/sdk/communication/azure-communication-identity/samples/identity_samples.py @@ -19,11 +19,12 @@ 3) AZURE_CLIENT_SECRET - the secret of your active directory application 4) AZURE_TENANT_ID - the tenant ID of your active directory application 5) COMMUNICATION_M365_APP_ID - the application id of Microsoft 365 - 6) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of Microsoft 365 + 6) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of Microsoft 365 7) COMMUNICATION_M365_AAD_TENANT - the tenant ID of Microsoft 365 application 8) COMMUNICATION_MSAL_USERNAME - the username for authenticating via MSAL library 9) COMMUNICATION_MSAL_PASSWORD - the password for authenticating via MSAL library """ + from datetime import timedelta import os from azure.communication.identity._shared.utils import parse_connection_str diff --git a/sdk/communication/azure-communication-identity/samples/identity_samples_async.py b/sdk/communication/azure-communication-identity/samples/identity_samples_async.py index b7f74a8f2c7b..80aa5fce4cd8 100644 --- a/sdk/communication/azure-communication-identity/samples/identity_samples_async.py +++ b/sdk/communication/azure-communication-identity/samples/identity_samples_async.py @@ -20,11 +20,12 @@ 4) AZURE_CLIENT_SECRET - the secret of your active directory application 5) AZURE_TENANT_ID - the tenant ID of your active directory application 6) COMMUNICATION_M365_APP_ID - the application id of Microsoft 365 - 7) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of M365 + 7) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of M365 8) COMMUNICATION_M365_AAD_TENANT - the tenant ID of Microsoft 365 application 9) COMMUNICATION_MSAL_USERNAME - the username for authenticating via the MSAL library 10) COMMUNICATION_MSAL_PASSWORD - the password for authenticating via the MSAL library """ + from datetime import timedelta from azure.communication.identity._shared.utils import parse_connection_str from msal import PublicClientApplication diff --git a/sdk/communication/azure-communication-identity/sdk_packaging.toml b/sdk/communication/azure-communication-identity/sdk_packaging.toml deleted file mode 100644 index 88dbadd37a99..000000000000 --- a/sdk/communication/azure-communication-identity/sdk_packaging.toml +++ /dev/null @@ -1,7 +0,0 @@ -[packaging] -auto_update = false -package_name = "azure-communication-identity" -package_pprint_name = "Communication Identity Service" -package_doc_id = "" -is_stable = false -is_arm = false \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py b/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py index 2502a4713a88..e552ff83a69a 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py index 6fdda359342e..c13cafb32f00 100644 --- a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py +++ b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py @@ -10,9 +10,7 @@ from azure.core.pipeline import PipelineRequest from azure.core.pipeline.transport import HttpRequest from unittest.mock import MagicMock -from azure.communication.identity._shared.entra_token_guard_policy import ( - EntraTokenGuardPolicy -) +from azure.communication.identity._shared.entra_token_guard_policy import EntraTokenGuardPolicy class DummyResponse: @@ -20,8 +18,7 @@ def __init__(self, status_code=200, expires_on=None): self.http_response = MagicMock() self.http_response.status_code = status_code self.http_response.text.return_value = ( - '{"accessToken": {"expiresOn": "%s"}}' % expires_on - if expires_on else '{}' + '{"accessToken": {"expiresOn": "%s"}}' % expires_on if expires_on else "{}" ) @@ -33,8 +30,7 @@ def setUp(self): def test_send_cache_miss(self): self.policy.next.send.return_value = DummyResponse( - status_code=200, - expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() + status_code=200, expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() ) self.request.http_request.headers["Authorization"] = "token" response = self.policy.send(self.request) @@ -45,8 +41,7 @@ def test_send_cache_hit(self): # Simulate a valid cache self.policy._entra_token_cache = "token" self.policy._response_cache = DummyResponse( - status_code=200, - expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() + status_code=200, expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() ) self.request.http_request.headers["Authorization"] = "token" response = self.policy.send(self.request) diff --git a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py index 1e1b452c746e..076870e4cc3c 100644 --- a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py @@ -21,7 +21,7 @@ def __init__(self, status_code=200, expires_on=None): def text(self): if self._expires_on: return '{"accessToken": {"expiresOn": "%s"}}' % self._expires_on - return '{}' + return "{}" class DummyPipelineResponse: @@ -66,4 +66,4 @@ async def test_send_invalid_response_raises(self): request = DummyRequest("token1") with pytest.raises(RuntimeError): - await policy.send(request) \ No newline at end of file + await policy.send(request) diff --git a/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py b/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py index c259bcda6ca8..413abb22a6d6 100644 --- a/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py +++ b/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py @@ -9,6 +9,7 @@ import unittest from azure.communication.identity import * + class IdentifierRawIdTest(unittest.TestCase): def test_raw_id(self): _assert_raw_id( @@ -98,42 +99,42 @@ def test_raw_id(self): ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="PUBLIC" + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="PUBLIC", ), "8:acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="DOD" + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="DOD", ), "8:dod-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="GCCH" + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="GCCH", ), "8:gcch-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="PUBLIC", - raw_id="8:extension:legacyFormat" + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="PUBLIC", + raw_id="8:extension:legacyFormat", ), "8:extension:legacyFormat", ) - + def test_identifier_from_raw_id(self): _assert_communication_identifier( "8:acs:bbbcbc1e-9f06-482a-b5d8-20e3f26ef0cd_45ab2481-1c1c-4005-be24-0ffb879b1130", @@ -213,70 +214,64 @@ def test_identifier_from_raw_id(self): MicrosoftTeamsAppIdentifier(app_id="45ab2481-1c1c-4005-be24-0ffb879b1130", cloud="GCCH"), ) _assert_phonenumber_identifier( - "4:+112345556789", - PhoneNumberIdentifier(value="+112345556789"), - withIsAnonymous=False, - withAssertedId=False) + "4:+112345556789", PhoneNumberIdentifier(value="+112345556789"), withIsAnonymous=False, withAssertedId=False + ) _assert_phonenumber_identifier( - "4:112345556789", - PhoneNumberIdentifier(value="112345556789"), - withIsAnonymous=False, - withAssertedId=False) + "4:112345556789", PhoneNumberIdentifier(value="112345556789"), withIsAnonymous=False, withAssertedId=False + ) _assert_phonenumber_identifier( - "4:otherFormat", - PhoneNumberIdentifier(value="otherFormat"), - withIsAnonymous=False, - withAssertedId=False) + "4:otherFormat", PhoneNumberIdentifier(value="otherFormat"), withIsAnonymous=False, withAssertedId=False + ) _assert_phonenumber_identifier( "4:207ffef6-9444-41fb-92ab-20eacaae2768", PhoneNumberIdentifier(value="207ffef6-9444-41fb-92ab-20eacaae2768"), withIsAnonymous=False, - withAssertedId=False + withAssertedId=False, ) # cspell:disable _assert_phonenumber_identifier( "4:207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", - PhoneNumberIdentifier(value="207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", - raw_id="4:207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768"), + PhoneNumberIdentifier( + value="207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", + raw_id="4:207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", + ), withIsAnonymous=False, - withAssertedId=True + withAssertedId=True, ) _assert_phonenumber_identifier( "4:anonymous", PhoneNumberIdentifier(value="anonymous", raw_id="4:anonymous"), - withIsAnonymous=True, withAssertedId=False + withIsAnonymous=True, + withAssertedId=False, ) _assert_phonenumber_identifier( - "4:+112345556789", - PhoneNumberIdentifier(value="+112345556789"), - withIsAnonymous=False, - withAssertedId=False + "4:+112345556789", PhoneNumberIdentifier(value="+112345556789"), withIsAnonymous=False, withAssertedId=False ) _assert_communication_identifier( "8:acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="PUBLIC", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="PUBLIC", ), ) _assert_communication_identifier( "8:dod-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="DOD", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="DOD", ), ) _assert_communication_identifier( "8:gcch-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="GCCH", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="GCCH", ), ) # cspell:enable @@ -424,36 +419,36 @@ def test_equality_based_on_raw_id(self): user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) == TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="DOD" + cloud="DOD", ) == TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="DOD" + cloud="DOD", ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="GCCH" + cloud="GCCH", ) == TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="GCCH" + cloud="GCCH", ) # TeamsExtensionUserIdentifiers are not equal. @@ -461,50 +456,50 @@ def test_equality_based_on_raw_id(self): user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) != TeamsExtensionUserIdentifier( user_id="55ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) != TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant456", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) != TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource456", - cloud="PUBLIC" + cloud="PUBLIC", ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC" + cloud="PUBLIC", ) != TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="DOD" + cloud="DOD", ) - + # UnknownIdentifiers are equal. assert UnknownIdentifier(identifier="28:ag08-global:01234567-89ab-cdef-0123-456789abcdef") == UnknownIdentifier( identifier="28:ag08-global:01234567-89ab-cdef-0123-456789abcdef" @@ -531,6 +526,7 @@ def _assert_communication_identifier(raw_id, want): assert key in got.properties assert got.properties[key] == want.properties[key] + def _assert_phonenumber_identifier(raw_id, want, withIsAnonymous=False, withAssertedId=False): # type: (str, PhoneNumberIdentifier, Optional[dict]) -> None got = identifier_from_raw_id(raw_id) diff --git a/sdk/communication/azure-communication-identity/tests/test_token_exchange.py b/sdk/communication/azure-communication-identity/tests/test_token_exchange.py index 2a5f036e8559..4917a79d8b66 100644 --- a/sdk/communication/azure-communication-identity/tests/test_token_exchange.py +++ b/sdk/communication/azure-communication-identity/tests/test_token_exchange.py @@ -9,9 +9,7 @@ from azure.core.credentials import AccessToken from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from unittest.mock import MagicMock -from azure.communication.identity._shared.token_exchange import ( - TokenExchangeClient -) +from azure.communication.identity._shared.token_exchange import TokenExchangeClient class DummyPipelineResponse: @@ -23,7 +21,8 @@ def __init__(self, status_code=200, token="tok", expires_on="2999-12-31T23:59:59 else: self.http_response.text.return_value = ( f'{{"accessToken": {{"token": "{token}", "expiresOn": "{expires_on}"}}}}' - if status_code == 200 else '{}' + if status_code == 200 + else "{}" ) diff --git a/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py b/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py index 25a67799c075..adb5b5c4df3c 100644 --- a/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py @@ -10,9 +10,7 @@ from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from unittest.mock import MagicMock, AsyncMock -from azure.communication.identity._shared.token_exchange_async import ( - TokenExchangeClient -) +from azure.communication.identity._shared.token_exchange_async import TokenExchangeClient class DummyPipelineResponse: @@ -24,7 +22,8 @@ def __init__(self, status_code=200, token="tok", expires_on="2999-12-31T23:59:59 else: self.http_response.text.return_value = ( f'{{"accessToken": {{"token": "{token}", "expiresOn": "{expires_on}"}}}}' - if status_code == 200 else '{}' + if status_code == 200 + else "{}" ) diff --git a/sdk/communication/azure-communication-identity/tests/test_token_utils.py b/sdk/communication/azure-communication-identity/tests/test_token_utils.py index 0857dc5961e4..c2eceb6258bb 100644 --- a/sdk/communication/azure-communication-identity/tests/test_token_utils.py +++ b/sdk/communication/azure-communication-identity/tests/test_token_utils.py @@ -28,8 +28,7 @@ def __init__(self, status_code=200, expires_on=None): self.http_response = MagicMock() self.http_response.status_code = status_code self.http_response.text.return_value = ( - '{"accessToken": {"expiresOn": "%s"}}' % expires_on - if expires_on else '{}' + '{"accessToken": {"expiresOn": "%s"}}' % expires_on if expires_on else "{}" ) @@ -71,21 +70,20 @@ def test_create_request_message_and_uri(self): assert uri.startswith("https://endpoint") assert TEAMS_EXTENSION_ENDPOINT in uri - req = utils.create_request_message("https://endpoint", - [TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) + req = utils.create_request_message("https://endpoint", [TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) assert req.method == "POST" assert req.headers["Accept"] == "application/json" assert req.headers["Content-Type"] == "application/json" def test_determine_endpoint_and_api_version_teams_extension(self): - endpoint, api_version = utils.determine_endpoint_and_api_version( - [TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) + endpoint, api_version = utils.determine_endpoint_and_api_version([TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) assert endpoint == TEAMS_EXTENSION_ENDPOINT assert api_version == TEAMS_EXTENSION_API_VERSION def test_determine_endpoint_and_api_version_communication_clients(self): endpoint, api_version = utils.determine_endpoint_and_api_version( - [COMMUNICATION_CLIENTS_SCOPE_PREFIX + ".default"]) + [COMMUNICATION_CLIENTS_SCOPE_PREFIX + ".default"] + ) assert endpoint == COMMUNICATION_CLIENTS_ENDPOINT assert api_version == COMMUNICATION_CLIENTS_API_VERSION diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 9598cecbe71d..c74d225ceef0 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -39,7 +39,7 @@ def __init__(self, resource_endpoint, token_credential, scopes): def exchange_entra_token(self): return DummyToken("dummy", 9999999999) - + class DummyTokenExchangeClientSwitch: def __init__(self, resource_endpoint, token_credential, scopes): @@ -251,11 +251,11 @@ def test_missing_fields_raises_value_error(self): def test_all_fields_present_calls_token_exchange(monkeypatch): # Patch TokenExchangeClient to our dummy - with patch("azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient): + with patch( + "azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient + ): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", - token_credential=MagicMock(), - scopes=["scope"] + resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] ) token = cred.get_token() assert token.token == "dummy" @@ -263,11 +263,10 @@ def test_all_fields_present_calls_token_exchange(monkeypatch): def test_missing_scopes_calls_token_exchange(monkeypatch): # Patch TokenExchangeClient to our dummy - with patch("azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient): - cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", - token_credential=MagicMock() - ) + with patch( + "azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient + ): + cred = CommunicationTokenCredential(resource_endpoint="https://endpoint", token_credential=MagicMock()) token = cred.get_token() assert token.token == "dummy" assert token.expires_on == 9999999999 @@ -275,11 +274,11 @@ def test_missing_scopes_calls_token_exchange(monkeypatch): def test_token_exchange_refreshes_from_expired_to_valid(monkeypatch): # Patch TokenExchangeClient to DummyTokenExchangeClientSwitch # First call returns expired token - when initializing the token - with patch("azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClientSwitch): + with patch( + "azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClientSwitch + ): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", - token_credential=MagicMock(), - scopes=["scope"] + resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] ) # Second call will trigger a refresh and generate a valid token token2 = cred.get_token() diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 80d33af1840c..3713158f3d4b 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -23,11 +23,13 @@ from azure.communication.identity._shared.utils import get_current_utc_as_int from _shared.helper import generate_token_with_custom_expiry + class DummyAsyncToken: def __init__(self, token, expires_on): self.token = token self.expires_on = expires_on + class DummyAsyncTokenExchangeClient: def __init__(self, resource_endpoint, token_credential, scopes): self.resource_endpoint = resource_endpoint @@ -37,6 +39,7 @@ def __init__(self, resource_endpoint, token_credential, scopes): async def exchange_entra_token(self): return DummyAsyncToken("dummy", 9999999999) + class DummyAsyncTokenExchangeClientSwitch: def __init__(self, resource_endpoint, token_credential, scopes): self.resource_endpoint = resource_endpoint @@ -283,13 +286,11 @@ async def test_missing_fields_raises_value_error(self): @pytest.mark.asyncio async def test_all_fields_present_calls_token_exchange_async(self): with patch( - "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", - DummyAsyncTokenExchangeClient, + "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", + DummyAsyncTokenExchangeClient, ): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", - token_credential=MagicMock(), - scopes=["scope"] + resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] ) token = await cred.get_token() assert token.token == "dummy" @@ -298,13 +299,10 @@ async def test_all_fields_present_calls_token_exchange_async(self): @pytest.mark.asyncio async def test_missing_scopes_calls_token_exchange_async(self): with patch( - "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", - DummyAsyncTokenExchangeClient, + "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", + DummyAsyncTokenExchangeClient, ): - cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", - token_credential=MagicMock() - ) + cred = CommunicationTokenCredential(resource_endpoint="https://endpoint", token_credential=MagicMock()) token = await cred.get_token() assert token.token == "dummy" assert token.expires_on == 9999999999 @@ -313,13 +311,11 @@ async def test_missing_scopes_calls_token_exchange_async(self): async def test_token_exchange_refreshes_from_expired_to_valid_async(self): # First call returns expired token on initialization of _token with patch( - "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", - DummyAsyncTokenExchangeClientSwitch, + "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", + DummyAsyncTokenExchangeClientSwitch, ): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", - token_credential=MagicMock(), - scopes=["scope"] + resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] ) # Second call returns valid token diff --git a/sdk/communication/azure-communication-identity/tests/test_utils.py b/sdk/communication/azure-communication-identity/tests/test_utils.py index 3e648881f5e9..0c875abbbcbc 100644 --- a/sdk/communication/azure-communication-identity/tests/test_utils.py +++ b/sdk/communication/azure-communication-identity/tests/test_utils.py @@ -27,21 +27,15 @@ def test_convert_datetime_to_utc_int(self): assert utc_naive_time_in_sec == 0 # PST is UTC-8 (Vancouver/Pacific Time) pst_tz = timezone(timedelta(hours=-8)) # PST is UTC-8 - pst_time_in_sec = _convert_datetime_to_utc_int( - datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pst_tz) - ) + pst_time_in_sec = _convert_datetime_to_utc_int(datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pst_tz)) assert pst_time_in_sec == 8 * 3600 # EST is UTC-5 (Eastern Time) est_tz = timezone(timedelta(hours=-5)) # EST is UTC-5 - est_time_in_sec = _convert_datetime_to_utc_int( - datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=est_tz) - ) + est_time_in_sec = _convert_datetime_to_utc_int(datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=est_tz)) assert est_time_in_sec == 5 * 3600 # CST is UTC+8 (China Standard Time) cst_tz = timezone(timedelta(hours=8)) # CST is UTC+8 - cst_time_in_sec = _convert_datetime_to_utc_int( - datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=cst_tz) - ) + cst_time_in_sec = _convert_datetime_to_utc_int(datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=cst_tz)) assert cst_time_in_sec == -8 * 3600 def test_access_token_expiry_deserialized_correctly_from_payload(self): diff --git a/sdk/communication/azure-communication-identity/tsp-location.yaml b/sdk/communication/azure-communication-identity/tsp-location.yaml new file mode 100644 index 000000000000..ffe6b6c58998 --- /dev/null +++ b/sdk/communication/azure-communication-identity/tsp-location.yaml @@ -0,0 +1,4 @@ +directory: specification/communication/data-plane/Identity +commit: 1893171028aed5a757fc78c2d4aa439c14df4afc +repo: Azure/azure-rest-api-specs +additionalDirectories: From 4407fdee1728bea5b02c1e1f43825f0da608f15f Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 3 Sep 2026 09:52:35 -0700 Subject: [PATCH 02/16] ACS Identity: rebase convenience layer onto DPG client Adapt the hand-written sync and async clients to the DPG-generated service layer while keeping the public API surface unchanged. Generated-shape changes absorbed here: - Client class renamed CommunicationIdentityClient -> IdentityClient. - One operation group became three (identity_operations, teams_user_operations, teams_extension_operations), matching the three TypeSpec interfaces. - Enum renamed CommunicationTokenScope -> CommunicationIdentityTokenScope; aliased back so the public name is unchanged. Two behaviors deliberately preserved against generator changes: 1. AccessToken.expires_on stays the raw service string. TypeSpec declares expiresOn as utcDateTime, so attribute access now deserializes to datetime. Reading the key instead returns the untouched wire value. Note .isoformat() is not equivalent: it changes the format and truncates 7-digit fractional seconds to 6. 2. expiresInMinutes is omitted when not supplied. msrest dropped None fields during serialization; the generated code forwards the request body as-is, which would send an explicit null for a property the service constrains to [60, 1440]. Default API version moves 2023-10-01 -> 2025-06-30. Verified: 97 offline unit tests pass, identical to the pre-migration baseline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure-communication-identity/CHANGELOG.md | 18 +- .../azure/communication/identity/__init__.py | 48 +++- .../communication/identity/_api_versions.py | 18 ++ .../_communication_identity_client.py | 218 ++++++++++++++++++ .../azure/communication/identity/_utils.py | 65 ++++++ .../_communication_identity_client_async.py | 214 +++++++++++++++++ 6 files changed, 578 insertions(+), 3 deletions(-) create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index a2fc948b32c1..289162c51b66 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -1,9 +1,23 @@ # Release History -## 1.6.0 (2026-09-09) +## 1.6.0 (Unreleased) -skip changelog generation for data-plane package and please add changelog manually. +### Features Added + +- Added support for a new communication identifier `TeamsExtensionUserIdentifier`. + - New identifier maps rawIds with format `8:acs:{resourceId}_{tenantId}_{userId}`. + - With this version, rawId starting with `8:acs` may be either `CommunicationUserIdentifier` or new `TeamsExtensionUserIdentifier`. +- Added `IsAnonymous` and `AssertedId` properties to the communication identifier `PhoneNumberIdentifier`. + - `IsAnonymous` is used for anonymous numbers with rawId equals to `4:anonymous`. + - `AssertedId` is used when the same number is used several times in the same call. It contains value after the last underscore `_` character in the phone number. It is null otherwise. + +### Other Changes +- The service layer is now generated from TypeSpec instead of Swagger/AutoRest. There are no + changes to the public API surface: method names, parameters, and return types are unchanged, + and `AccessToken.expires_on` continues to carry the expiry string exactly as returned by the + service. +- The default service API version is now `2025-06-30` (previously `2023-10-01`). ## 1.5.1 (Unreleased) ### Features Added diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py index d55ccad1f573..c23199142080 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py @@ -1 +1,47 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from ._communication_identity_client import CommunicationIdentityClient +from ._generated.models import CommunicationIdentityTokenScope as CommunicationTokenScope + +from ._shared.models import ( + CommunicationCloudEnvironment, + CommunicationIdentifier, + CommunicationIdentifierKind, + CommunicationUserIdentifier, + CommunicationUserProperties, + identifier_from_raw_id, + MicrosoftTeamsAppIdentifier, + MicrosoftTeamsAppProperties, + MicrosoftTeamsUserIdentifier, + MicrosoftTeamsUserProperties, + PhoneNumberIdentifier, + PhoneNumberProperties, + UnknownIdentifier, + TeamsExtensionUserIdentifier, + TeamsExtensionUserProperties, +) + +__all__ = [ + "CommunicationIdentityClient", + # from _identity + "CommunicationTokenScope", + # from _shared + "CommunicationCloudEnvironment", + "CommunicationIdentifier", + "CommunicationIdentifierKind", + "CommunicationUserIdentifier", + "CommunicationUserProperties", + "identifier_from_raw_id", + "MicrosoftTeamsAppIdentifier", + "MicrosoftTeamsAppProperties", + "MicrosoftTeamsUserIdentifier", + "MicrosoftTeamsUserProperties", + "PhoneNumberIdentifier", + "PhoneNumberProperties", + "UnknownIdentifier", + "TeamsExtensionUserIdentifier", + "TeamsExtensionUserProperties", +] diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py new file mode 100644 index 000000000000..ce1cf363ab0f --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py @@ -0,0 +1,18 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from enum import Enum +from azure.core import CaseInsensitiveEnumMeta + + +class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): + V2021_03_07 = "2021-03-07" + V2022_06_01 = "2022-06-01" + V2022_10_01 = "2022-10-01" + V2023_10_01 = "2023-10-01" + V2025_06_30 = "2025-06-30" + + +DEFAULT_VERSION = ApiVersion.V2025_06_30 diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py new file mode 100644 index 000000000000..61ed5711ccfe --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -0,0 +1,218 @@ +# coding=utf-8 +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from datetime import timedelta +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union +from azure.core.tracing.decorator import distributed_trace +from azure.core.credentials import AccessToken +from ._generated._client import ( + IdentityClient as CommunicationIdentityClientGen, +) +from ._shared.auth_policy_utils import get_authentication_policy +from ._shared.utils import parse_connection_str +from ._shared.models import CommunicationUserIdentifier +from ._version import SDK_MONIKER +from ._api_versions import DEFAULT_VERSION +from ._utils import build_token_request_body, to_access_token +from ._generated.models import CommunicationIdentityTokenScope as CommunicationTokenScope + +if TYPE_CHECKING: + from azure.core.credentials import TokenCredential, AzureKeyCredential + + +class CommunicationIdentityClient(object): + """Azure Communication Services Identity client. + + :param str endpoint: + The endpoint url for Azure Communication Service resource. + :param Union[TokenCredential, AzureKeyCredential] credential: + The credential we use to authenticate against the service. + :keyword api_version: Azure Communication Identity API version. + Default value is "2022-10-01". Note that overriding this default value may result in unsupported behavior. + :paramtype api_version: str + + .. admonition:: Example: + + .. literalinclude:: ../samples/identity_samples.py + :language: python + """ + + def __init__( + self, + endpoint: str, + credential, # type: Union[TokenCredential, AzureKeyCredential] + **kwargs # type: Any + ) -> None: + try: + if not endpoint.lower().startswith("http"): + endpoint = "https://" + endpoint + except AttributeError as err: + raise ValueError("Account URL must be a string.") from err + + if not credential: + raise ValueError("You need to provide account shared key to authenticate.") + self._endpoint = endpoint + self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) + self._identity_service_client = CommunicationIdentityClientGen( + self._endpoint, + api_version=self._api_version, + authentication_policy=get_authentication_policy(endpoint, credential), + sdk_moniker=SDK_MONIKER, + **kwargs + ) + + @classmethod + def from_connection_string( + cls, + conn_str: str, + **kwargs: Any + ) -> "CommunicationIdentityClient": + """Create CommunicationIdentityClient from a Connection String. + + :param str conn_str: A connection string to an Azure Communication Service resource. + :returns: Instance of CommunicationIdentityClient. + :rtype: ~azure.communication.identity.CommunicationIdentityClient + """ + endpoint, access_key = parse_connection_str(conn_str) + + # There is logic downstream in method `get_authentication_policy` to handle string credential. + # Marking this as type: ignore to resolve mypy warning. + return cls(endpoint, access_key, **kwargs) # type: ignore + + @distributed_trace + def create_user(self, **kwargs) -> CommunicationUserIdentifier: + """create a single Communication user + + :return: CommunicationUserIdentifier + :rtype: ~azure.communication.identity.CommunicationUserIdentifier + """ + identity_access_token = self._identity_service_client.identity_operations.create(**kwargs) + + return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) + + @distributed_trace + def create_user_and_token( + self, + scopes: List[Union[str, CommunicationTokenScope]], *, + token_expires_in: Optional[timedelta] = None, + **kwargs: Any + ) -> Tuple[CommunicationUserIdentifier, AccessToken]: + """Create a single Communication user with an identity token. + + :param scopes: List of scopes to be added to the token. + :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] + :keyword token_expires_in: Custom validity period of the Communication Identity access token + within [1, 24] hours range. If not provided, the default value of 24 hours will be used. + :paramtype token_expires_in: ~datetime.timedelta + :return: A tuple of a CommunicationUserIdentifier and a AccessToken. + :rtype: + tuple of (~azure.communication.identity.CommunicationUserIdentifier, ~azure.core.credentials.AccessToken) + """ + request_body = build_token_request_body("createTokenWithScopes", scopes, token_expires_in) + identity_access_token = self._identity_service_client.identity_operations.create( + body=request_body, **kwargs # type: ignore + ) + + user_identifier = CommunicationUserIdentifier( + identity_access_token.identity.id, raw_id=identity_access_token.identity.id + ) + access_token = to_access_token(identity_access_token["accessToken"]) + + return user_identifier, access_token + + @distributed_trace + def delete_user( + self, + user: CommunicationUserIdentifier, + **kwargs: Any + ) -> None: + """Triggers revocation event for user and deletes all its data. + + :param user: Azure Communication User to delete + :type user: ~azure.communication.identity.CommunicationUserIdentifier + :return: None + :rtype: None + """ + self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) + + @distributed_trace + def get_token( + self, + user: CommunicationUserIdentifier, + scopes: List[Union[str, CommunicationTokenScope]], *, + token_expires_in: Optional[timedelta] = None, + **kwargs: Any + ) -> AccessToken: + """Generates a new token for an identity. + + :param user: Azure Communication User + :type user: ~azure.communication.identity.CommunicationUserIdentifier + :param scopes: List of scopes to be added to the token. + :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] + :keyword token_expires_in: Custom validity period of the Communication Identity access token + within [1, 24] hours range. If not provided, the default value of 24 hours will be used. + :paramtype token_expires_in: ~datetime.timedelta + :return: AccessToken + :rtype: ~azure.core.credentials.AccessToken + """ + request_body = build_token_request_body("scopes", scopes, token_expires_in) + + access_token = self._identity_service_client.identity_operations.issue_access_token( + user.properties["id"], body=request_body, **kwargs # type: ignore + ) + + return to_access_token(access_token) + + @distributed_trace + def revoke_tokens( + self, + user: CommunicationUserIdentifier, + **kwargs: Any + ) -> None: + """Schedule revocation of all tokens of an identity. + + :param user: Azure Communication User. + :type user: ~azure.communication.identity.CommunicationUserIdentifier. + :return: None + :rtype: None + """ + return self._identity_service_client.identity_operations.revoke_access_tokens( + user.properties["id"] if user else None, **kwargs # type: ignore + ) + + @distributed_trace + def get_token_for_teams_user( + self, + aad_token: str, + client_id: str, + user_object_id: str, + **kwargs: Any + ) -> AccessToken: + """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. + + :param aad_token: an Azure AD access token of a Teams User. + :type aad_token: str + :param client_id: a Client ID of an Azure AD application to be verified against + the appId claim in the Azure AD access token. + :type client_id: str + :param user_object_id: an Object ID of an Azure AD user (Teams User) to be verified against + the OID claim in the Azure AD access token. + :type user_object_id: str + :return: AccessToken + :rtype: ~azure.core.credentials.AccessToken + """ + + request_body = { + "token": aad_token, + "appId": client_id, + "userId": user_object_id, + } + + access_token = self._identity_service_client.teams_user_operations.exchange_teams_user_access_token( + body=request_body, **kwargs # type: ignore + ) + + return to_access_token(access_token) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py new file mode 100644 index 000000000000..445ef47196aa --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py @@ -0,0 +1,65 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from datetime import timedelta +from typing import Any, Dict, List, Optional, Union +from azure.core.credentials import AccessToken + + +def to_access_token(access_token: Any) -> AccessToken: + """Build an AccessToken from a generated CommunicationIdentityAccessToken. + + ``expiresOn`` is read by key rather than via the ``.expires_on`` attribute. The + TypeSpec model types it as ``utcDateTime``, so attribute access deserializes it to a + ``datetime``, whereas the previously published SDK exposed the raw string. Reading the + key returns the untouched wire value, keeping the public surface unchanged. + + :param access_token: A generated CommunicationIdentityAccessToken instance. + :type access_token: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :return: The access token and its expiry, as returned by the service. + :rtype: ~azure.core.credentials.AccessToken + """ + return AccessToken(access_token["token"], access_token["expiresOn"]) + + +def build_token_request_body( + scopes_key: str, + scopes: List[Union[str, Any]], + token_expires_in: Optional[timedelta], +) -> Dict[str, Any]: + """Build a token request body, omitting the expiry when it was not requested. + + ``expiresInMinutes`` is only included when the caller supplied a value. The previous + msrest-based layer dropped ``None`` fields during serialization, whereas the generated + code now forwards the request body as-is, which would put an explicit ``null`` on the + wire for a property the service constrains to [60, 1440]. + + :param scopes_key: Name of the scopes property expected by the service. + :type scopes_key: str + :param scopes: List of scopes to be added to the token. + :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] + :param token_expires_in: Optional custom validity period of the token. + :type token_expires_in: ~datetime.timedelta or None + :return: The request body to send to the service. + :rtype: dict[str, any] + """ + request_body: Dict[str, Any] = {scopes_key: scopes} + expires_in_minutes = convert_timedelta_to_mins(token_expires_in) + if expires_in_minutes is not None: + request_body["expiresInMinutes"] = expires_in_minutes + return request_body + + +def convert_timedelta_to_mins( + duration: Optional[timedelta], +) -> Optional[int]: + """ + Returns the total number of minutes contained in the duration. + :param duration: Time duration + :type duration: ~datetime.timedelta + :rtype: int + """ + return None if duration is None else int(duration.total_seconds() / 60) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py new file mode 100644 index 000000000000..5c3c1cafdc07 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -0,0 +1,214 @@ +# coding=utf-8 +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from datetime import timedelta +from typing import List, Optional, Union, Tuple +from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.credentials import AccessToken +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.credentials import AzureKeyCredential +from .._generated.aio._client import ( + IdentityClient as CommunicationIdentityClientGen, +) +from .._shared.auth_policy_utils import get_authentication_policy +from .._shared.utils import parse_connection_str +from .._shared.models import CommunicationUserIdentifier +from .._version import SDK_MONIKER +from .._api_versions import DEFAULT_VERSION +from .._utils import build_token_request_body, to_access_token +from .._generated.models import CommunicationIdentityTokenScope as CommunicationTokenScope + + +class CommunicationIdentityClient: + """Azure Communication Services Identity client. + + :param str endpoint: + The endpoint url for Azure Communication Service resource. + :param Union[AsyncTokenCredential, AzureKeyCredential] credential: + The credential we use to authenticate against the service. + :keyword api_version: Azure Communication Identity API version. + Default value is "2022-06-01". Note that overriding this default value may result in unsupported behavior. + :paramtype api_version: str + + .. admonition:: Example: + + .. literalinclude:: ../samples/identity_samples_async.py + :language: python + """ + + def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureKeyCredential], **kwargs) -> None: + try: + if not endpoint.lower().startswith("http"): + endpoint = "https://" + endpoint + except AttributeError as err: + raise ValueError("Account URL must be a string.") from err + + if not credential: + raise ValueError("You need to provide account shared key to authenticate.") + + self._endpoint = endpoint + self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) + self._identity_service_client = CommunicationIdentityClientGen( + self._endpoint, + api_version=self._api_version, + authentication_policy=get_authentication_policy(endpoint, credential, decode_url=True, is_async=True), + sdk_moniker=SDK_MONIKER, + **kwargs + ) + + @classmethod + def from_connection_string(cls, conn_str: str, **kwargs) -> "CommunicationIdentityClient": + """Create CommunicationIdentityClient from a Connection String. + + :param str conn_str: + A connection string to an Azure Communication Service resource. + :returns: Instance of CommunicationIdentityClient. + :rtype: ~azure.communication.identity.aio.CommunicationIdentityClient + """ + endpoint, access_key = parse_connection_str(conn_str) + + # There is logic downstream in method `get_authentication_policy` to handle string credential. + # Marking this as type: ignore to resolve mypy warning. + return cls(endpoint, access_key, **kwargs) # type: ignore + + @distributed_trace_async + async def create_user(self, **kwargs) -> "CommunicationUserIdentifier": + """create a single Communication user + + :return: CommunicationUserIdentifier + :rtype: ~azure.communication.identity.CommunicationUserIdentifier + """ + identity_access_token = await self._identity_service_client.identity_operations.create(**kwargs) + + return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) + + @distributed_trace_async + async def create_user_and_token( + self, scopes: List[Union[str, CommunicationTokenScope]], *, + token_expires_in: Optional[timedelta] = None, + **kwargs + ) -> Tuple["CommunicationUserIdentifier", AccessToken]: + """Create a single Communication user with an identity token. + + :param scopes: + List of scopes to be added to the token. + :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] + :keyword token_expires_in: Custom validity period of the Communication Identity access token + within [1, 24] hours range. If not provided, the default value of 24 hours will be used. + :paramtype token_expires_in: ~datetime.timedelta + :return: A tuple of a CommunicationUserIdentifier and a AccessToken. + :rtype: + tuple of (~azure.communication.identity.CommunicationUserIdentifier, ~azure.core.credentials.AccessToken) + """ + request_body = build_token_request_body("createTokenWithScopes", scopes, token_expires_in) + identity_access_token = await self._identity_service_client.identity_operations.create( + body=request_body, **kwargs # type: ignore + ) + + user_identifier = CommunicationUserIdentifier( + identity_access_token.identity.id, raw_id=identity_access_token.identity.id + ) + access_token = to_access_token(identity_access_token["accessToken"]) + + return user_identifier, access_token + + @distributed_trace_async + async def delete_user(self, user: CommunicationUserIdentifier, **kwargs) -> None: + """Triggers revocation event for user and deletes all its data. + + :param user: + Azure Communication User to delete + :type user: ~azure.communication.identity.CommunicationUserIdentifier + :return: None + :rtype: None + """ + await self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) + + @distributed_trace_async + async def get_token( + self, user: CommunicationUserIdentifier, + scopes: List[Union[str, CommunicationTokenScope]], *, + token_expires_in: Optional[timedelta] = None, + **kwargs + ) -> AccessToken: + """Generates a new token for an identity. + + :param user: Azure Communication User + :type user: ~azure.communication.identity.CommunicationUserIdentifier + :param scopes: + List of scopes to be added to the token. + :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] + :keyword ~datetime.timedelta token_expires_in: Custom validity period of the Communication Identity access token + within [1, 24] hours range. If not provided, the default value of 24 hours will be used. + :return: AccessToken + :rtype: ~azure.core.credentials.AccessToken + """ + request_body = build_token_request_body("scopes", scopes, token_expires_in) + + access_token = await self._identity_service_client.identity_operations.issue_access_token( + user.properties["id"], body=request_body, **kwargs # type: ignore + ) + + return to_access_token(access_token) + + @distributed_trace_async + async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> None: + """Schedule revocation of all tokens of an identity. + + :param user: Azure Communication User. + :type user: ~azure.communication.identity.CommunicationUserIdentifier + :return: None + :rtype: None + """ + return await self._identity_service_client.identity_operations.revoke_access_tokens( + user.properties["id"] if user else None, **kwargs # type: ignore + ) + + @distributed_trace_async + async def get_token_for_teams_user( + self, + aad_token, # type: str + client_id, # type: str + user_object_id, # type: str + **kwargs + ): + # type: (...) -> AccessToken + """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. + + :param aad_token: an Azure AD access token of a Teams User + :type aad_token: str + :param client_id: a Client ID of an Azure AD application to be verified against + the appId claim in the Azure AD access token. + :type client_id: str + :param user_object_id: an Object ID of an Azure AD user (Teams User) to be verified against + the OID claim in the Azure AD access token. + :type user_object_id: str + :return: AccessToken + :rtype: ~azure.core.credentials.AccessToken + """ + request_body = { + "token": aad_token, + "appId": client_id, + "userId": user_object_id, + } + access_token = await self._identity_service_client.teams_user_operations.exchange_teams_user_access_token( + body=request_body, **kwargs # type: ignore + ) + + return to_access_token(access_token) + + async def __aenter__(self) -> "CommunicationIdentityClient": + await self._identity_service_client.__aenter__() + return self + + async def __aexit__(self, *args) -> None: + await self.close() + + async def close(self) -> None: + """Close the :class: + `~azure.communication.identity.aio.CommunicationIdentityClient` session. + """ + await self._identity_service_client.__aexit__() From 6c061b1ed443e6d96fc391e6ffaa32859a7f4d9a Mon Sep 17 00:00:00 2001 From: John Fraser Date: Fri, 4 Sep 2026 12:33:57 -0700 Subject: [PATCH 03/16] ACS Identity: release 2.0.0 targeting api-version 2026-09-23 Add V2026_09_23 to the ApiVersion enum and make it the default. Bump the package to 2.0.0, a coordinated major increment across all four language SDKs rather than a semver-driven one: the public API surface is unchanged and the 97 offline unit tests pass exactly as before. Re-verified after regeneration: - expiresInMinutes is still omitted when not supplied and present when it is, sync and async. This is the build_token_request_body fix; msrest used to drop None fields, and without it an explicit null reaches a property the service constrains to [60, 1440]. - AccessToken.expires_on is still the raw service string, byte-identical. - Every public attribute on every returned object reads back populated, sync and async, including the identifier properties and kind. - api-version=2026-09-23 confirmed on the wire. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure-communication-identity/CHANGELOG.md | 6 ++++-- .../azure/communication/identity/_api_versions.py | 3 ++- .../azure/communication/identity/_version.py | 12 ++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index 289162c51b66..e5b3d02f1713 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -1,9 +1,11 @@ # Release History -## 1.6.0 (Unreleased) +## 2.0.0 (Unreleased) ### Features Added +- Added support for service API version `2026-09-23`, which is now the default. This version is + functionally equivalent to `2025-06-30`. - Added support for a new communication identifier `TeamsExtensionUserIdentifier`. - New identifier maps rawIds with format `8:acs:{resourceId}_{tenantId}_{userId}`. - With this version, rawId starting with `8:acs` may be either `CommunicationUserIdentifier` or new `TeamsExtensionUserIdentifier`. @@ -17,7 +19,7 @@ changes to the public API surface: method names, parameters, and return types are unchanged, and `AccessToken.expires_on` continues to carry the expiry string exactly as returned by the service. -- The default service API version is now `2025-06-30` (previously `2023-10-01`). + ## 1.5.1 (Unreleased) ### Features Added diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py index ce1cf363ab0f..3a5a5ac8f13b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py @@ -13,6 +13,7 @@ class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): V2022_10_01 = "2022-10-01" V2023_10_01 = "2023-10-01" V2025_06_30 = "2025-06-30" + V2026_09_23 = "2026-09-23" -DEFAULT_VERSION = ApiVersion.V2025_06_30 +DEFAULT_VERSION = ApiVersion.V2026_09_23 diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py index 4e2942f574c4..563541b7b0a4 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py @@ -1,9 +1,9 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- +# ------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# Licensed under the MIT License. See License.txt in the project root for +# license information. # -------------------------------------------------------------------------- -VERSION = "1.6.0" +VERSION = "2.0.0" + +SDK_MONIKER = f"communication-identity/{VERSION}" # type: str From 699525db5cd288df49807344114b6d15a90d5d12 Mon Sep 17 00:00:00 2001 From: John Fraser Date: Tue, 8 Sep 2026 19:36:55 -0700 Subject: [PATCH 04/16] ACS Identity: document why expiresOn is read by key The AutoRest configuration that recorded this constraint was deleted with swagger/SWAGGER.md, so to_access_token is now the only place the reasoning survives. Record it there: that returning a datetime would break callers, that this already happened once under AutoRest, and that .isoformat() is not an adequate substitute because it reformats and truncates fractional seconds. Comment only - no behavior change. 97 tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure/communication/identity/_utils.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py index 445ef47196aa..439772597a08 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py @@ -12,10 +12,23 @@ def to_access_token(access_token: Any) -> AccessToken: """Build an AccessToken from a generated CommunicationIdentityAccessToken. - ``expiresOn`` is read by key rather than via the ``.expires_on`` attribute. The - TypeSpec model types it as ``utcDateTime``, so attribute access deserializes it to a - ``datetime``, whereas the previously published SDK exposed the raw string. Reading the - key returns the untouched wire value, keeping the public surface unchanged. + ``expiresOn`` is read **by key**, not via the ``.expires_on`` attribute. Do not + "simplify" this to attribute access: the two are not equivalent. + + The TypeSpec model declares ``expiresOn: utcDateTime``, so the generated model types it + as ``datetime.datetime`` and attribute access returns a deserialized ``datetime``. Every + previously published version of this SDK exposed the raw service string, so returning a + ``datetime`` would be a breaking change for callers. Item access on the generated model + returns the untouched wire value, which keeps the public surface unchanged. + + This is a re-occurrence, not a new concern. The AutoRest configuration formerly at + ``swagger/SWAGGER.md`` carried a directive deleting ``format: date-time`` from this same + property, added because a generator upgrade had already turned it from string into + ``datetime`` once before. That directive acted on the swagger document and does not carry + over to TypeSpec, so this function is now the only thing preventing the same break. + + ``.isoformat()`` is **not** an adequate substitute: it reformats the value (``Z`` becomes + ``+00:00``) and silently truncates the service's 7-digit fractional seconds to 6. :param access_token: A generated CommunicationIdentityAccessToken instance. :type access_token: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken From dc5c474d7dfd18e86a5e2080cd955f88eacd3e8a Mon Sep 17 00:00:00 2001 From: John Fraser Date: Wed, 9 Sep 2026 10:58:37 -0700 Subject: [PATCH 05/16] ACS Identity: add committed wire-contract tests Every wire check performed during this migration was an ad hoc probe. None was committed, so nothing in CI asserted the three behaviours the DPG move silently changed. Grepping tests/ for api-version returned nothing. Adds 9 tests covering, at the transport layer: - expiresInMinutes omitted when no expiry is requested, present when one is - AccessToken.expires_on carrying the raw service string, not a datetime - the targeted api-version reaching the wire on all six operations Each assertion is mutation-tested. Reverting build_token_request_body fails 2, reverting to_access_token to attribute access fails 3, and reverting DEFAULT_VERSION fails 2. That last figure was 1 before this commit. Mutation testing showed the per-operation check compared the wire against DEFAULT_VERSION itself, which is self-referential and cannot detect a wrong default. It now asserts a literal. 106 offline tests pass (97 existing plus these 9). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/test_wire_contract.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 sdk/communication/azure-communication-identity/tests/test_wire_contract.py diff --git a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py new file mode 100644 index 000000000000..61655567eab8 --- /dev/null +++ b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py @@ -0,0 +1,197 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +"""Wire-contract tests for CommunicationIdentityClient. + +These assert what the client actually puts on the wire, rather than what the +convenience layer returns. They exist because the move from AutoRest to the +TypeSpec DPG emitter changed three things that are invisible to an API-surface +check and are not covered by the recorded tests: + +1. ``expiresInMinutes`` must be omitted when the caller supplies no expiry. + msrest dropped ``None`` fields during serialization; the generated code + forwards the request body as-is, so without a fix an explicit ``null`` + reaches a property the service constrains to [60, 1440]. + +2. ``AccessToken.expires_on`` must remain the raw service string. The TypeSpec + model declares ``expiresOn: utcDateTime``, so attribute access on the + generated model returns a ``datetime``; every published version of this SDK + exposed the string. + +3. The default api-version must be the one the package claims to target. + +Requests are captured at the transport, which is the last pipeline stage before +the network, so these are wire observations rather than serializer return +values. +""" + +import json +from datetime import timedelta + +import pytest +from azure.core.pipeline.transport import HttpTransport + +from azure.communication.identity import ( + CommunicationIdentityClient, + CommunicationTokenScope, +) +from azure.communication.identity._api_versions import DEFAULT_VERSION +from azure.communication.identity._shared.models import CommunicationUserIdentifier + +WIRE_EXPIRY = "2026-09-23T12:00:00.1234567Z" +EXPECTED_API_VERSION = "2026-09-23" +FAKE_ENDPOINT = "https://sanitized.communication.azure.com" +FAKE_KEY = "a2V5" * 8 + + +class _FakeResponse: + def __init__(self, request, status_code, body): + self.request = request + self.status_code = status_code + self._body = json.dumps(body).encode() if body is not None else b"" + self.headers = {"content-type": "application/json"} + self.reason = "OK" + self.content_type = "application/json" + self.is_closed = True + self.is_stream_consumed = True + + @property + def content(self): + return self._body + + def text(self, encoding=None): + return self._body.decode() + + def json(self): + return json.loads(self._body) + + def read(self): + return self._body + + def close(self): + pass + + def raise_for_status(self): + pass + + +class _CapturingTransport(HttpTransport): + """Records each outgoing request and replies with a canned response.""" + + def __init__(self): + self.requests = [] + + def send(self, request, **kwargs): + self.requests.append(request) + token = {"token": "jwt.tok.sig", "expiresOn": WIRE_EXPIRY} + path = request.url.split("?")[0] + if path.endswith("/identities"): + return _FakeResponse(request, 201, {"identity": {"id": "8:acs:u"}, "accessToken": token}) + if path.endswith(":issueAccessToken") or path.endswith("teamsUser/:exchangeAccessToken"): + return _FakeResponse(request, 200, token) + return _FakeResponse(request, 204, None) + + def open(self): + pass + + def close(self): + pass + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + +@pytest.fixture +def transport(): + return _CapturingTransport() + + +@pytest.fixture +def client(transport): + return CommunicationIdentityClient(FAKE_ENDPOINT, FAKE_KEY, transport=transport) + + +def _sent_body(transport): + return json.loads(transport.requests[-1].content) + + +class TestRequestBody: + """expiresInMinutes must be absent unless the caller asked for an expiry.""" + + def test_create_user_and_token_omits_expiry_when_not_requested(self, client, transport): + client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) + body = _sent_body(transport) + assert "expiresInMinutes" not in body + assert body["createTokenWithScopes"] == ["chat"] + + def test_create_user_and_token_sends_expiry_when_requested(self, client, transport): + client.create_user_and_token( + scopes=[CommunicationTokenScope.CHAT], + token_expires_in=timedelta(hours=2), + ) + assert _sent_body(transport)["expiresInMinutes"] == 120 + + def test_get_token_omits_expiry_when_not_requested(self, client, transport): + client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.VOIP]) + body = _sent_body(transport) + assert "expiresInMinutes" not in body + assert body["scopes"] == ["voip"] + + def test_get_token_sends_expiry_when_requested(self, client, transport): + client.get_token( + CommunicationUserIdentifier("8:acs:u"), + scopes=[CommunicationTokenScope.VOIP], + token_expires_in=timedelta(hours=3), + ) + assert _sent_body(transport)["expiresInMinutes"] == 180 + + +class TestAccessTokenExpiry: + """expires_on must be the untouched service string, not a deserialized datetime.""" + + def test_create_user_and_token_expiry_is_raw_string(self, client): + _, token = client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) + assert isinstance(token.expires_on, str) + assert token.expires_on == WIRE_EXPIRY + + def test_get_token_expiry_is_raw_string(self, client): + token = client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.CHAT]) + assert isinstance(token.expires_on, str) + assert token.expires_on == WIRE_EXPIRY + + def test_get_token_for_teams_user_expiry_is_raw_string(self, client): + token = client.get_token_for_teams_user("aad-token", "client-id", "user-object-id") + assert isinstance(token.expires_on, str) + assert token.expires_on == WIRE_EXPIRY + + +class TestApiVersion: + """Every request must carry the api-version the package targets. + + The expected version is asserted as a literal rather than by reading + ``DEFAULT_VERSION``. Comparing the wire against the same constant that + produced it is self-referential and cannot detect a wrong default -- that + weakness was found by mutation testing, where changing ``DEFAULT_VERSION`` + left the loop below passing. + """ + + def test_default_api_version_is_sent_on_every_operation(self, client, transport): + user = CommunicationUserIdentifier("8:acs:u") + client.create_user() + client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) + client.get_token(user, scopes=[CommunicationTokenScope.CHAT]) + client.get_token_for_teams_user("aad-token", "client-id", "user-object-id") + client.revoke_tokens(user) + client.delete_user(user) + + assert len(transport.requests) == 6 + for request in transport.requests: + assert f"api-version={EXPECTED_API_VERSION}" in request.url + + def test_default_api_version_matches_shipped_value(self): + assert DEFAULT_VERSION.value == EXPECTED_API_VERSION From 2f5fc3519b7a2b86a3aac9fac7462d23544c6006 Mon Sep 17 00:00:00 2001 From: John Fraser Date: Wed, 9 Sep 2026 16:40:34 -0700 Subject: [PATCH 06/16] ACS Identity: restore three request headers dropped by DPG The TypeSpec emitter drops three request headers the AutoRest client sent: Content-Type on the bodyless create_user POST, and Accept on revoke_tokens and delete_user. The service is indifferent to all three - measured against a live resource - but a proxy, gateway or request log keying on headers would observe the change, so under the black-box-identical requirement they are restored. Accept is restored by passing headers= through to the generated operation. Content-Type could not be. The generated create operation nulls content_type whenever no body is present, twice: content_type = content_type if body else None content_type = content_type or "application/json" if body else None so no argument reaches the request builder. It is restored by a SansIOHTTPPolicy matched narrowly to a bodyless POST to the identities collection, leaving the body-bearing create_user_and_token untouched. That same discard also drops a caller-supplied Content-Type, which the AutoRest client honoured - a second regression found while testing the first. create_user now moves any caller value into the request context for the policy to reapply. Verified: all six operations compared against the pre-migration client, sync and async, for accept and content-type - zero mismatches. create_user still sends no body. 15 wire tests (6 new), each mutation-tested: removing the policy fails 2, neutering the Accept merge fails 2. 112 offline tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../_communication_identity_client.py | 54 +++++------ .../azure/communication/identity/_utils.py | 94 +++++++++++++++++++ .../_communication_identity_client_async.py | 34 +++++-- .../tests/test_wire_contract.py | 46 +++++++++ 4 files changed, 193 insertions(+), 35 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py index 61ed5711ccfe..2cfc1a97648c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -16,8 +16,15 @@ from ._shared.models import CommunicationUserIdentifier from ._version import SDK_MONIKER from ._api_versions import DEFAULT_VERSION -from ._utils import build_token_request_body, to_access_token -from ._generated.models import CommunicationIdentityTokenScope as CommunicationTokenScope +from ._utils import ( + _ACCEPT_JSON, + BodylessCreateContentTypePolicy, + build_token_request_body, + extract_create_content_type, + merge_headers, + to_access_token, +) +from ._generated.models import CommunicationTokenScope if TYPE_CHECKING: from azure.core.credentials import TokenCredential, AzureKeyCredential @@ -56,20 +63,19 @@ def __init__( raise ValueError("You need to provide account shared key to authenticate.") self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) + per_call_policies = list(kwargs.pop("per_call_policies", None) or []) + per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential), sdk_moniker=SDK_MONIKER, + per_call_policies=per_call_policies, **kwargs ) @classmethod - def from_connection_string( - cls, - conn_str: str, - **kwargs: Any - ) -> "CommunicationIdentityClient": + def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "CommunicationIdentityClient": """Create CommunicationIdentityClient from a Connection String. :param str conn_str: A connection string to an Azure Communication Service resource. @@ -89,14 +95,17 @@ def create_user(self, **kwargs) -> CommunicationUserIdentifier: :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = self._identity_service_client.identity_operations.create(**kwargs) + identity_access_token = self._identity_service_client.identity_operations.create( + **extract_create_content_type(kwargs) + ) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace def create_user_and_token( self, - scopes: List[Union[str, CommunicationTokenScope]], *, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> Tuple[CommunicationUserIdentifier, AccessToken]: @@ -124,11 +133,7 @@ def create_user_and_token( return user_identifier, access_token @distributed_trace - def delete_user( - self, - user: CommunicationUserIdentifier, - **kwargs: Any - ) -> None: + def delete_user(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: """Triggers revocation event for user and deletes all its data. :param user: Azure Communication User to delete @@ -136,13 +141,16 @@ def delete_user( :return: None :rtype: None """ - self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) + self._identity_service_client.identity_operations.delete( + user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) + ) @distributed_trace def get_token( self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], *, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> AccessToken: @@ -167,11 +175,7 @@ def get_token( return to_access_token(access_token) @distributed_trace - def revoke_tokens( - self, - user: CommunicationUserIdentifier, - **kwargs: Any - ) -> None: + def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: """Schedule revocation of all tokens of an identity. :param user: Azure Communication User. @@ -180,16 +184,12 @@ def revoke_tokens( :rtype: None """ return self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **kwargs # type: ignore + user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore ) @distributed_trace def get_token_for_teams_user( - self, - aad_token: str, - client_id: str, - user_object_id: str, - **kwargs: Any + self, aad_token: str, client_id: str, user_object_id: str, **kwargs: Any ) -> AccessToken: """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py index 439772597a08..10cd1220fc29 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py @@ -7,6 +7,26 @@ from datetime import timedelta from typing import Any, Dict, List, Optional, Union from azure.core.credentials import AccessToken +from azure.core.pipeline.policies import SansIOHTTPPolicy + +# Headers the AutoRest-generated client sent that the TypeSpec DPG emitter does not. +# +# Restoring them keeps the request black-box identical to the previously published SDK. The +# service itself is indifferent -- measured against a live resource, `Accept` makes no +# difference to either the success or the error path -- but a proxy, gateway or request log +# keying on headers would observe the change, so the difference is customer-visible even +# though the service ignores it. +# +# Why the emitter drops them: +# - `delete` and `revoke_access_tokens` return 204 with no response body, so the emitter +# emits no `Accept` header for them at all. +# - `create` declares `content_type` only as a body-parameter keyword, so with no body +# passed it is never applied. +# +# These are request headers only. Sending `Content-Type` does not cause a body to be sent; +# the body remains absent for `create_user`, as it was under AutoRest. +_ACCEPT_JSON = {"Accept": "application/json"} +_CONTENT_TYPE_JSON = {"Content-Type": "application/json"} def to_access_token(access_token: Any) -> AccessToken: @@ -66,6 +86,80 @@ def build_token_request_body( return request_body +def extract_create_content_type(kwargs: Dict[str, Any]) -> Dict[str, Any]: + """Preserve a caller-supplied ``Content-Type`` across the generated create operation. + + The operation discards it when no body is present, so it is moved into the + request context for :class:`BodylessCreateContentTypePolicy` to reapply. + + :param kwargs: Keyword arguments destined for the create operation. + :type kwargs: dict[str, any] + :return: The keyword arguments, with any Content-Type moved to the context. + :rtype: dict[str, any] + """ + headers = kwargs.get("headers") or {} + override = kwargs.pop("content_type", None) + for name in list(headers): + if name.lower() == "content-type": + override = headers.pop(name) + if override: + kwargs[BodylessCreateContentTypePolicy.CONTEXT_KEY] = override + return kwargs + + +def merge_headers(kwargs: Dict[str, Any], defaults: Dict[str, str]) -> Dict[str, Any]: + """Add default request headers without overriding any the caller supplied. + + :param kwargs: Keyword arguments destined for a generated operation. + :type kwargs: dict[str, any] + :param defaults: Headers to apply when the caller has not set them. + :type defaults: dict[str, str] + :return: The keyword arguments, with a merged ``headers`` entry. + :rtype: dict[str, any] + """ + headers = dict(kwargs.pop("headers", None) or {}) + existing = {name.lower() for name in headers} + for name, value in defaults.items(): + if name.lower() not in existing: + headers[name] = value + kwargs["headers"] = headers + return kwargs + + +class BodylessCreateContentTypePolicy(SansIOHTTPPolicy): + """Restores ``Content-Type`` on the bodyless identity-create request. + + The generated ``create`` operation discards ``content_type`` whenever no body + is present -- twice, via ``content_type if body else None`` and again via + ``content_type or "application/json" if body else None`` -- so no argument + passed to the operation can survive. A policy is therefore the only way to + restore the header without editing generated code. + + The same discard also drops a caller-supplied ``Content-Type``, which the + AutoRest client honoured. ``CommunicationIdentityClient.create_user`` + consequently stashes any caller value on the request context under + ``_acs_create_content_type`` and this policy reapplies it, so an explicit + override still reaches the wire. + + The match is deliberately narrow: a POST to the identities collection with no + body. ``create_user_and_token`` sends a body and so already carries the header + from the generated code, and is left untouched. + """ + + CONTEXT_KEY = "_acs_create_content_type" + + def on_request(self, request) -> None: + http_request = request.http_request + if http_request.method != "POST": + return + if not http_request.url.split("?")[0].endswith("/identities"): + return + if http_request.body: + return + override = request.context.options.pop(self.CONTEXT_KEY, None) + http_request.headers["Content-Type"] = override or "application/json" + + def convert_timedelta_to_mins( duration: Optional[timedelta], ) -> Optional[int]: diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py index 5c3c1cafdc07..6fdc6bc78932 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -18,8 +18,15 @@ from .._shared.models import CommunicationUserIdentifier from .._version import SDK_MONIKER from .._api_versions import DEFAULT_VERSION -from .._utils import build_token_request_body, to_access_token -from .._generated.models import CommunicationIdentityTokenScope as CommunicationTokenScope +from .._utils import ( + _ACCEPT_JSON, + BodylessCreateContentTypePolicy, + build_token_request_body, + extract_create_content_type, + merge_headers, + to_access_token, +) +from .._generated.models import CommunicationTokenScope class CommunicationIdentityClient: @@ -51,11 +58,14 @@ def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureK self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) + per_call_policies = list(kwargs.pop("per_call_policies", None) or []) + per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential, decode_url=True, is_async=True), sdk_moniker=SDK_MONIKER, + per_call_policies=per_call_policies, **kwargs ) @@ -81,13 +91,17 @@ async def create_user(self, **kwargs) -> "CommunicationUserIdentifier": :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = await self._identity_service_client.identity_operations.create(**kwargs) + identity_access_token = await self._identity_service_client.identity_operations.create( + **extract_create_content_type(kwargs) + ) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace_async async def create_user_and_token( - self, scopes: List[Union[str, CommunicationTokenScope]], *, + self, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> Tuple["CommunicationUserIdentifier", AccessToken]: @@ -125,12 +139,16 @@ async def delete_user(self, user: CommunicationUserIdentifier, **kwargs) -> None :return: None :rtype: None """ - await self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) + await self._identity_service_client.identity_operations.delete( + user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) + ) @distributed_trace_async async def get_token( - self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], *, + self, + user: CommunicationUserIdentifier, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> AccessToken: @@ -164,7 +182,7 @@ async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> No :rtype: None """ return await self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **kwargs # type: ignore + user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore ) @distributed_trace_async diff --git a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py index 61655567eab8..f390ce948b7f 100644 --- a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py +++ b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py @@ -170,6 +170,52 @@ def test_get_token_for_teams_user_expiry_is_raw_string(self, client): assert token.expires_on == WIRE_EXPIRY +class TestRequestHeaders: + """Request headers must match what the AutoRest-generated client sent. + + The TypeSpec DPG emitter drops three headers the previous client sent: + ``Content-Type`` on the bodyless ``create_user`` POST, and ``Accept`` on + ``revoke_tokens`` and ``delete_user`` (both typed as 204-no-content, for + which the emitter produces no ``Accept``). + + The service is indifferent to these -- measured against a live resource -- + but a proxy, gateway or request log keying on headers would observe the + change, so they are restored in the convenience layer. + + Expected values are literals rather than references to the constants that + produce them: asserting against ``_ACCEPT_JSON`` would compare the wire to + the same value that set it and would pass even if both were wrong. + """ + + def test_create_user_sends_content_type(self, client, transport): + client.create_user() + assert transport.requests[-1].headers["Content-Type"] == "application/json" + + def test_create_user_sends_no_body_despite_content_type(self, client, transport): + """Content-Type must not cause a body to be sent -- AutoRest sent none.""" + client.create_user() + assert transport.requests[-1].content is None + + def test_revoke_tokens_sends_accept(self, client, transport): + client.revoke_tokens(CommunicationUserIdentifier("8:acs:u")) + assert transport.requests[-1].headers["Accept"] == "application/json" + + def test_delete_user_sends_accept(self, client, transport): + client.delete_user(CommunicationUserIdentifier("8:acs:u")) + assert transport.requests[-1].headers["Accept"] == "application/json" + + def test_body_bearing_operations_still_send_both_headers(self, client, transport): + client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) + headers = transport.requests[-1].headers + assert headers["Accept"] == "application/json" + assert headers["Content-Type"] == "application/json" + + def test_caller_supplied_content_type_is_not_overridden(self, client, transport): + """The policy must defer to a Content-Type already on the request.""" + client.create_user(headers={"Content-Type": "application/custom"}) + assert transport.requests[-1].headers["Content-Type"] == "application/custom" + + class TestApiVersion: """Every request must carry the api-version the package targets. From 26724d9e70c02a9a930b59db98e6fbc81c871dfc Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 09:58:30 -0700 Subject: [PATCH 07/16] ACS Identity: restore files the pipeline rmtree deleted _shared/ (13 files) and aio/__init__.py were removed by the generation pipeline's unscoped rmtree of the package namespace directory. They are unmodified by this migration, so no convenience-layer commit carries them; they are restored verbatim from the merge-base. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../identity/_shared/__init__.py | 5 + .../identity/_shared/auth_policy_utils.py | 53 ++ .../_shared/entra_token_guard_policy.py | 30 + .../_shared/entra_token_guard_policy_async.py | 30 + .../communication/identity/_shared/models.py | 522 ++++++++++++++++++ .../communication/identity/_shared/policy.py | 118 ++++ .../identity/_shared/token_exchange.py | 78 +++ .../identity/_shared/token_exchange_async.py | 82 +++ .../identity/_shared/token_utils.py | 91 +++ .../identity/_shared/user_credential.py | 213 +++++++ .../identity/_shared/user_credential_async.py | 225 ++++++++ .../communication/identity/_shared/utils.py | 89 +++ .../identity/_shared/utils_async.py | 31 ++ .../communication/identity/aio/__init__.py | 9 + 14 files changed, 1576 insertions(+) create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py new file mode 100644 index 000000000000..5b396cd202e8 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py new file mode 100644 index 000000000000..c176b112f9b1 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/auth_policy_utils.py @@ -0,0 +1,53 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from typing import Union, cast +from azure.core.credentials import TokenCredential, AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.pipeline.policies import ( + AsyncBearerTokenCredentialPolicy, + BearerTokenCredentialPolicy, +) +from .._shared.policy import HMACCredentialsPolicy + + +def get_authentication_policy( + endpoint: str, + credential: Union[TokenCredential, AsyncTokenCredential, AzureKeyCredential, str], + decode_url: bool = False, + is_async: bool = False, +) -> Union[AsyncBearerTokenCredentialPolicy, BearerTokenCredentialPolicy, HMACCredentialsPolicy]: + """Returns the correct authentication policy based on which credential is being passed. + + :param endpoint: The endpoint to which we are authenticating to. + :type endpoint: str + :param credential: The credential we use to authenticate to the service + :type credential: Union[TokenCredential, AsyncTokenCredential, AzureKeyCredential, str] + :param bool decode_url: `True` if there is a need to decode the url. Default value is `False` + :param bool is_async: For async clients there is a need to decode the url + :return: The authentication policy to be used. + :rtype: ~azure.core.pipeline.policies.AsyncBearerTokenCredentialPolicy or + ~azure.core.pipeline.policies.BearerTokenCredentialPolicy or + ~.HMACCredentialsPolicy + """ + + if credential is None: + raise ValueError("Parameter 'credential' must not be None.") + if hasattr(credential, "get_token"): + if is_async: + return AsyncBearerTokenCredentialPolicy( + cast(AsyncTokenCredential, credential), "https://communication.azure.com//.default" + ) + return BearerTokenCredentialPolicy( + cast(TokenCredential, credential), "https://communication.azure.com//.default" + ) + if isinstance(credential, (AzureKeyCredential, str)): + return HMACCredentialsPolicy(endpoint, credential, decode_url=decode_url) + + raise TypeError( + f"Unsupported credential: {format(type(credential))}. Use an access token string to use HMACCredentialsPolicy" + "or a token credential from azure.identity" + ) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py new file mode 100644 index 000000000000..651378179892 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from azure.core.pipeline.policies import HTTPPolicy +from azure.core.pipeline import PipelineRequest +from . import token_utils + + +class EntraTokenGuardPolicy(HTTPPolicy): + """A pipeline policy that caches the response for a given Entra token and reuses it if valid.""" + + def __init__(self): + super().__init__() + self._entra_token_cache = None + self._response_cache = None + + def send(self, request: PipelineRequest): + cache_valid, token = token_utils.is_entra_token_cache_valid(self._entra_token_cache, request) + if cache_valid and token_utils.is_acs_token_cache_valid(self._response_cache): + response = self._response_cache + else: + self._entra_token_cache = token + response = self.next.send(request) + self._response_cache = response + if response is None: + raise RuntimeError("Failed to obtain a valid PipelineResponse in EntraTokenGuardPolicy.send") + return response diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py new file mode 100644 index 000000000000..195324bbe28e --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy_async.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline import PipelineRequest +from . import token_utils + + +class EntraTokenGuardPolicy(AsyncHTTPPolicy): + """Async pipeline policy that caches the response for a given Entra token and reuses it if valid.""" + + def __init__(self): + super().__init__() + self._entra_token_cache = None + self._response_cache = None + + async def send(self, request: PipelineRequest): + cache_valid, token = token_utils.is_entra_token_cache_valid(self._entra_token_cache, request) + if cache_valid and token_utils.is_acs_token_cache_valid(self._response_cache): + response = self._response_cache + else: + self._entra_token_cache = token + response = await self.next.send(request) + self._response_cache = response + if response is None: + raise RuntimeError("Failed to obtain a valid PipelineResponse in AsyncEntraTokenGuardPolicy.send") + return response diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py new file mode 100644 index 000000000000..d71d8428504d --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/models.py @@ -0,0 +1,522 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from enum import Enum +import warnings +from typing import Mapping, Optional, Union, Any, cast +from typing_extensions import Literal, TypedDict, Protocol, runtime_checkable, NotRequired + +from azure.core import CaseInsensitiveEnumMeta + + +class DeprecatedEnumMeta(CaseInsensitiveEnumMeta): + + def __getattribute__(cls, item): + if item.upper() == "MICROSOFT_BOT": + warnings.warn( + "MICROSOFT_BOT is deprecated and has been replaced by \ + MICROSOFT_TEAMS_APP identifier.", + DeprecationWarning, + ) + item = "MICROSOFT_TEAMS_APP" + return super().__getattribute__(item) + + +class CommunicationIdentifierKind(str, Enum, metaclass=DeprecatedEnumMeta): + """Communication Identifier Kind. + + For checking yet unknown identifiers it is better to rely on the presence of the `raw_id` property, + as new or existing distinct type identifiers always contain the `raw_id` property. + It is not advisable to rely on the `kind` property with a value `unknown`, + as it could become a new or existing distinct type in the future. + """ + + UNKNOWN = "unknown" + COMMUNICATION_USER = "communication_user" + PHONE_NUMBER = "phone_number" + MICROSOFT_TEAMS_USER = "microsoft_teams_user" + MICROSOFT_TEAMS_APP = "microsoft_teams_app" + TEAMS_EXTENSION_USER = "teams_extension_user" + + +class CommunicationCloudEnvironment(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """The cloud environment that the identifier belongs to""" + + PUBLIC = "PUBLIC" + DOD = "DOD" + GCCH = "GCCH" + + +@runtime_checkable +class CommunicationIdentifier(Protocol): + """Communication Identifier.""" + + @property + def raw_id(self) -> str: + """The raw ID of the identifier.""" + ... + + @property + def kind(self) -> CommunicationIdentifierKind: + """The type of identifier.""" + ... + + @property + def properties(self) -> Mapping[str, Any]: + """The properties of the identifier.""" + ... + + +PHONE_NUMBER_PREFIX = "4:" +BOT_PREFIX = "28:" +BOT_PUBLIC_CLOUD_PREFIX = "28:orgid:" +BOT_DOD_CLOUD_PREFIX = "28:dod:" +BOT_DOD_CLOUD_GLOBAL_PREFIX = "28:dod-global:" +BOT_GCCH_CLOUD_PREFIX = "28:gcch:" +BOT_GCCH_CLOUD_GLOBAL_PREFIX = "28:gcch-global:" +TEAMS_APP_PUBLIC_CLOUD_PREFIX = "28:orgid:" +TEAMS_APP_DOD_CLOUD_PREFIX = "28:dod:" +TEAMS_APP_GCCH_CLOUD_PREFIX = "28:gcch:" +TEAMS_USER_ANONYMOUS_PREFIX = "8:teamsvisitor:" +TEAMS_USER_PUBLIC_CLOUD_PREFIX = "8:orgid:" +TEAMS_USER_DOD_CLOUD_PREFIX = "8:dod:" +TEAMS_USER_GCCH_CLOUD_PREFIX = "8:gcch:" +ACS_USER_PREFIX = "8:acs:" +ACS_USER_DOD_CLOUD_PREFIX = "8:dod-acs:" +ACS_USER_GCCH_CLOUD_PREFIX = "8:gcch-acs:" +SPOOL_USER_PREFIX = "8:spool:" + +PHONE_NUMBER_ANONYMOUS_SUFFIX = "anonymous" + + +class CommunicationUserProperties(TypedDict): + """Dictionary of properties for a CommunicationUserIdentifier.""" + + id: str + """ID of the Communication user as returned from Azure Communication Identity.""" + + +class CommunicationUserIdentifier: + """Represents a user in Azure Communication Service.""" + + kind: Literal[CommunicationIdentifierKind.COMMUNICATION_USER] = CommunicationIdentifierKind.COMMUNICATION_USER + """The type of identifier.""" + properties: CommunicationUserProperties + """The properties of the identifier.""" + raw_id: str + """The raw ID of the identifier.""" + + def __init__(self, id: str, **kwargs: Any) -> None: + """ + :param str id: ID of the Communication user as returned from Azure Communication Identity. + :keyword str raw_id: The raw ID of the identifier. If not specified, the 'id' value will be used. + """ + self.properties = CommunicationUserProperties(id=id) + raw_id: Optional[str] = kwargs.get("raw_id") + self.raw_id = raw_id if raw_id is not None else id + + def __eq__(self, other): + try: + if other.raw_id: + return self.raw_id == other.raw_id + return self.raw_id == other.properties["id"] + except Exception: # pylint: disable=broad-except + return False + + +class PhoneNumberProperties(TypedDict): + """Dictionary of properties for a PhoneNumberIdentifier.""" + + value: str + """The phone number in E.164 format.""" + asserted_id: NotRequired[str] + """The asserted Id set on a phone number to distinguish from other connections made through the same number.""" + is_anonymous: NotRequired[bool] + """True if the phone number is anonymous, e.g. when used to represent a hidden caller Id.""" + + +class PhoneNumberIdentifier: + """Represents a phone number.""" + + kind: Literal[CommunicationIdentifierKind.PHONE_NUMBER] = CommunicationIdentifierKind.PHONE_NUMBER + """The type of identifier.""" + properties: PhoneNumberProperties + """The properties of the identifier.""" + raw_id: str + """The raw ID of the identifier.""" + + def __init__(self, value: str, **kwargs: Any) -> None: + """ + :param str value: The phone number. + :keyword str raw_id: The raw ID of the identifier. If not specified, this will be constructed from + the 'value' parameter. + """ + + raw_id: Optional[str] = kwargs.get("raw_id") + is_anonymous: bool + + if raw_id is not None: + phone_number = raw_id[len(PHONE_NUMBER_PREFIX) :] + is_anonymous = phone_number == PHONE_NUMBER_ANONYMOUS_SUFFIX + asserted_id_index = -1 if is_anonymous else phone_number.rfind("_") + 1 + has_asserted_id = 0 < asserted_id_index < len(phone_number) + if has_asserted_id: + self.properties = PhoneNumberProperties( + value=value, is_anonymous=is_anonymous, asserted_id=phone_number[asserted_id_index:] + ) + else: + self.properties = PhoneNumberProperties(value=value, is_anonymous=is_anonymous) + else: + self.properties = PhoneNumberProperties(value=value) + self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) + + def __eq__(self, other): + try: + if other.raw_id: + return self.raw_id == other.raw_id + return self.raw_id == self._format_raw_id(other.properties) + except Exception: # pylint:disable=broad-except + return False + + def _format_raw_id(self, properties: PhoneNumberProperties) -> str: + # We just assume correct E.164 format here because + # validation should only happen server-side, not client-side. + value = properties["value"] + return f"{PHONE_NUMBER_PREFIX}{value}" + + +class UnknownIdentifier: + """Represents an identifier of an unknown type. + + It will be encountered in communications with endpoints that are not + identifiable by this version of the SDK. + + For checking yet unknown identifiers it is better to rely on the presence of the `raw_id` property, + as new or existing distinct type identifiers always contain the `raw_id` property. + It is not advisable to rely on the `kind` property with a value `unknown`, + as it could become a new or existing distinct type in the future. + """ + + kind: Literal[CommunicationIdentifierKind.UNKNOWN] = CommunicationIdentifierKind.UNKNOWN + """The type of identifier.""" + properties: Mapping[str, Any] + """The properties of the identifier.""" + raw_id: str + """The raw ID of the identifier.""" + + def __init__(self, identifier: str) -> None: + """ + :param str identifier: The ID of the identifier. + """ + self.raw_id = identifier + self.properties = {} + + def __eq__(self, other): + try: + return self.raw_id == other.raw_id + except AttributeError: + return False + + +class MicrosoftTeamsUserProperties(TypedDict): + """Dictionary of properties for a MicrosoftTeamsUserIdentifier.""" + + user_id: str + """The id of the Microsoft Teams user. If the user isn't anonymous, the id is the AAD object id of the user.""" + is_anonymous: bool + """Set this to true if the user is anonymous for example when joining a meeting with a share link.""" + cloud: Union[CommunicationCloudEnvironment, str] + """Cloud environment that this identifier belongs to.""" + + +class MicrosoftTeamsUserIdentifier: + """Represents an identifier for a Microsoft Teams user.""" + + kind: Literal[CommunicationIdentifierKind.MICROSOFT_TEAMS_USER] = CommunicationIdentifierKind.MICROSOFT_TEAMS_USER + """The type of identifier.""" + properties: MicrosoftTeamsUserProperties + """The properties of the identifier.""" + raw_id: str + """The raw ID of the identifier.""" + + def __init__(self, user_id: str, **kwargs: Any) -> None: + """ + :param str user_id: Microsoft Teams user id. + :keyword bool is_anonymous: `True` if the identifier is anonymous. Default value is `False`. + :keyword cloud: Cloud environment that the user belongs to. Default value is `PUBLIC`. + :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` + :keyword str raw_id: The raw ID of the identifier. If not specified, this value will be constructed from + the other properties. + """ + self.properties = MicrosoftTeamsUserProperties( + user_id=user_id, + is_anonymous=kwargs.get("is_anonymous", False), + cloud=kwargs.get("cloud") or CommunicationCloudEnvironment.PUBLIC, + ) + raw_id: Optional[str] = kwargs.get("raw_id") + self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) + + def __eq__(self, other): + try: + if other.raw_id: + return self.raw_id == other.raw_id + return self.raw_id == self._format_raw_id(other.properties) + except Exception: # pylint: disable=broad-except + return False + + def _format_raw_id(self, properties: MicrosoftTeamsUserProperties) -> str: + user_id = properties["user_id"] + if properties["is_anonymous"]: + return f"{TEAMS_USER_ANONYMOUS_PREFIX}{user_id}" + cloud = properties["cloud"] + if cloud == CommunicationCloudEnvironment.DOD: + return f"{TEAMS_USER_DOD_CLOUD_PREFIX}{user_id}" + if cloud == CommunicationCloudEnvironment.GCCH: + return f"{TEAMS_USER_GCCH_CLOUD_PREFIX}{user_id}" + if cloud == CommunicationCloudEnvironment.PUBLIC: + return f"{TEAMS_USER_PUBLIC_CLOUD_PREFIX}{user_id}" + return f"{TEAMS_USER_PUBLIC_CLOUD_PREFIX}{user_id}" + + +class MicrosoftTeamsAppProperties(TypedDict): + """Dictionary of properties for a MicrosoftTeamsAppIdentifier.""" + + app_id: str + """The id of the Microsoft Teams application.""" + cloud: Union[CommunicationCloudEnvironment, str] + """Cloud environment that this identifier belongs to.""" + + +class _botbackcompatdict(dict): + """Backwards compatible properties.""" + + def __getitem__(self, __key: Any) -> Any: + try: + return super().__getitem__(__key) + except KeyError: + if __key == "bot_id": + return super().__getitem__("app_id") + if __key == "is_resource_account_configured": + return True + raise + + +class MicrosoftTeamsAppIdentifier: + """Represents an identifier for a Microsoft Teams application.""" + + kind: Literal[CommunicationIdentifierKind.MICROSOFT_TEAMS_APP] = CommunicationIdentifierKind.MICROSOFT_TEAMS_APP + """The type of identifier.""" + properties: MicrosoftTeamsAppProperties + """The properties of the identifier.""" + raw_id: str + """The raw ID of the identifier.""" + + def __init__(self, app_id: str, **kwargs: Any) -> None: + """ + :param str app_id: Microsoft Teams application id. + :keyword cloud: Cloud environment that the application belongs to. Default value is `PUBLIC`. + :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` + :keyword str raw_id: The raw ID of the identifier. If not specified, this value will be constructed + from the other properties. + """ + self.properties = cast( + MicrosoftTeamsAppProperties, + _botbackcompatdict( + app_id=app_id, + cloud=kwargs.get("cloud") or CommunicationCloudEnvironment.PUBLIC, + ), + ) + raw_id: Optional[str] = kwargs.get("raw_id") + self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) + + def __eq__(self, other): + try: + if other.raw_id: + return self.raw_id == other.raw_id + return self.raw_id == self._format_raw_id(other.properties) + except Exception: # pylint: disable=broad-except + return False + + def _format_raw_id(self, properties: MicrosoftTeamsAppProperties) -> str: + app_id = properties["app_id"] + cloud = properties["cloud"] + if cloud == CommunicationCloudEnvironment.DOD: + return f"{TEAMS_APP_DOD_CLOUD_PREFIX}{app_id}" + if cloud == CommunicationCloudEnvironment.GCCH: + return f"{TEAMS_APP_GCCH_CLOUD_PREFIX}{app_id}" + return f"{TEAMS_APP_PUBLIC_CLOUD_PREFIX}{app_id}" + + +class _MicrosoftBotIdentifier(MicrosoftTeamsAppIdentifier): + """Represents an identifier for a Microsoft bot. + + DEPRECATED. Only used in cases of backwards compatibility. + """ + + def __init__(self, bot_id, **kwargs): + """ + :param str bot_id: Microsoft bot id. + :keyword bool is_resource_account_configured: `False` if the identifier is global. + Default value is `True` for tennantzed bots. + :keyword cloud: Cloud environment that the bot belongs to. Default value is `PUBLIC`. + :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` + """ + warnings.warn( + "The MicrosoftBotIdentifier is deprecated and has been replaced by MicrosoftTeamsAppIdentifier.", + DeprecationWarning, + ) + super().__init__(bot_id, **kwargs) + + +class TeamsExtensionUserProperties(TypedDict): + """Dictionary of properties for a TeamsExtensionUserIdentifier.""" + + user_id: str + """The id of the Teams extension user.""" + tenant_id: str + """The tenant id associated with the user.""" + resource_id: str + """The Communication Services resource id.""" + cloud: Union[CommunicationCloudEnvironment, str] + """Cloud environment that this identifier belongs to.""" + + +class TeamsExtensionUserIdentifier: + """Represents an identifier for a Teams Extension user.""" + + kind: Literal[CommunicationIdentifierKind.TEAMS_EXTENSION_USER] = CommunicationIdentifierKind.TEAMS_EXTENSION_USER + """The type of identifier.""" + properties: TeamsExtensionUserProperties + """The properties of the identifier.""" + raw_id: str + """The raw ID of the identifier.""" + + def __init__(self, *, user_id: str, tenant_id: str, resource_id: str, **kwargs: Any) -> None: + """ + :param str user_id: Teams extension user id. + :param str tenant_id: Tenant id associated with the user. + :param str resource_id: The Communication Services resource id. + :keyword cloud: Cloud environment that the user belongs to. Default value is `PUBLIC`. + :paramtype cloud: str or :class:`~.CommunicationCloudEnvironment` + :keyword str raw_id: The raw ID of the identifier. + If not specified, this value will be constructed from the other properties. + """ + self.properties = TeamsExtensionUserProperties( + user_id=user_id, + tenant_id=tenant_id, + resource_id=resource_id, + cloud=kwargs.get("cloud") or CommunicationCloudEnvironment.PUBLIC, + ) + raw_id: Optional[str] = kwargs.get("raw_id") + self.raw_id = raw_id if raw_id is not None else self._format_raw_id(self.properties) + + def __eq__(self, other): + try: + if other.raw_id: + return self.raw_id == other.raw_id + return self.raw_id == self._format_raw_id(other.properties) + except Exception: # pylint: disable=broad-except + return False + + def _format_raw_id(self, properties: TeamsExtensionUserProperties) -> str: + # The prefix depends on the cloud + cloud = properties["cloud"] + if cloud == CommunicationCloudEnvironment.DOD: + prefix = ACS_USER_DOD_CLOUD_PREFIX + elif cloud == CommunicationCloudEnvironment.GCCH: + prefix = ACS_USER_GCCH_CLOUD_PREFIX + else: + prefix = ACS_USER_PREFIX + return f"{prefix}{properties['resource_id']}_{properties['tenant_id']}_{properties['user_id']}" + + +def try_create_teams_extension_user(prefix: str, suffix: str) -> Optional[TeamsExtensionUserIdentifier]: + segments = suffix.split("_") + if len(segments) != 3: + return None + resource_id, tenant_id, user_id = segments + if prefix == ACS_USER_PREFIX: + cloud = CommunicationCloudEnvironment.PUBLIC + elif prefix == ACS_USER_DOD_CLOUD_PREFIX: + cloud = CommunicationCloudEnvironment.DOD + elif prefix == ACS_USER_GCCH_CLOUD_PREFIX: + cloud = CommunicationCloudEnvironment.GCCH + else: + raise ValueError("Invalid MRI") + return TeamsExtensionUserIdentifier(user_id=user_id, tenant_id=tenant_id, resource_id=resource_id, cloud=cloud) + + +def identifier_from_raw_id(raw_id: str) -> CommunicationIdentifier: # pylint: disable=too-many-return-statements + """ + Creates a CommunicationIdentifier from a given raw ID. + + When storing raw IDs use this function to restore the identifier that was encoded in the raw ID. + + :param str raw_id: A raw ID to construct the CommunicationIdentifier from. + :return: The CommunicationIdentifier parsed from the raw_id. + :rtype: :class:`~.CommunicationIdentifier` + """ + if raw_id.startswith(PHONE_NUMBER_PREFIX): + return PhoneNumberIdentifier(value=raw_id[len(PHONE_NUMBER_PREFIX) :], raw_id=raw_id) + + segments = raw_id.split(":", maxsplit=2) + if len(segments) < 3: + return UnknownIdentifier(identifier=raw_id) + + prefix = f"{segments[0]}:{segments[1]}:" + suffix = segments[2] + if prefix == TEAMS_USER_ANONYMOUS_PREFIX: + return MicrosoftTeamsUserIdentifier(user_id=suffix, is_anonymous=True, raw_id=raw_id) + if prefix == TEAMS_USER_PUBLIC_CLOUD_PREFIX: + return MicrosoftTeamsUserIdentifier( + user_id=suffix, + is_anonymous=False, + cloud=CommunicationCloudEnvironment.PUBLIC, + raw_id=raw_id, + ) + if prefix == TEAMS_USER_DOD_CLOUD_PREFIX: + return MicrosoftTeamsUserIdentifier( + user_id=suffix, + is_anonymous=False, + cloud=CommunicationCloudEnvironment.DOD, + raw_id=raw_id, + ) + if prefix == TEAMS_USER_GCCH_CLOUD_PREFIX: + return MicrosoftTeamsUserIdentifier( + user_id=suffix, + is_anonymous=False, + cloud=CommunicationCloudEnvironment.GCCH, + raw_id=raw_id, + ) + if prefix == TEAMS_APP_PUBLIC_CLOUD_PREFIX: + return MicrosoftTeamsAppIdentifier( + app_id=suffix, + cloud=CommunicationCloudEnvironment.PUBLIC, + raw_id=raw_id, + ) + if prefix == TEAMS_APP_DOD_CLOUD_PREFIX: + return MicrosoftTeamsAppIdentifier( + app_id=suffix, + cloud=CommunicationCloudEnvironment.DOD, + raw_id=raw_id, + ) + if prefix == TEAMS_APP_GCCH_CLOUD_PREFIX: + return MicrosoftTeamsAppIdentifier( + app_id=suffix, + cloud=CommunicationCloudEnvironment.GCCH, + raw_id=raw_id, + ) + if prefix == SPOOL_USER_PREFIX: + return CommunicationUserIdentifier(id=raw_id, raw_id=raw_id) + + if prefix in [ + ACS_USER_PREFIX, + ACS_USER_DOD_CLOUD_PREFIX, + ACS_USER_GCCH_CLOUD_PREFIX, + ]: + identifier = try_create_teams_extension_user(prefix, suffix) + if identifier is not None: + return identifier + return CommunicationUserIdentifier(id=raw_id, raw_id=raw_id) + return UnknownIdentifier(identifier=raw_id) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py new file mode 100644 index 000000000000..6eae18574193 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/policy.py @@ -0,0 +1,118 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import hashlib +import base64 +import hmac +from urllib.parse import urlparse, unquote +from typing import Union + +from azure.core.credentials import AzureKeyCredential +from azure.core.pipeline.policies import SansIOHTTPPolicy +from azure.core.pipeline import PipelineRequest + +from .utils import get_current_utc_time + + +class HMACCredentialsPolicy(SansIOHTTPPolicy): + """Implementation of HMAC authentication policy. + + :param str host: The host of the endpoint url for Azure Communication Service resource + :param access_key: The access key we use to authenticate to the service + :type access_key: str or AzureKeyCredential + :param bool decode_url: `True` if there is a need to decode the url. Default value is `False` + """ + + def __init__( + self, + host: str, + access_key: Union[str, AzureKeyCredential], + decode_url: bool = False, + ) -> None: + super(HMACCredentialsPolicy, self).__init__() + + if host.startswith("https://"): + self._host = host.replace("https://", "") + + if host.startswith("http://"): + self._host = host.replace("http://", "") + + self._access_key = access_key + self._decode_url = decode_url + + def _compute_hmac(self, value: str) -> str: + if isinstance(self._access_key, AzureKeyCredential): + decoded_secret = base64.b64decode(self._access_key.key) + else: + decoded_secret = base64.b64decode(self._access_key) + + digest = hmac.new(decoded_secret, value.encode("utf-8"), hashlib.sha256).digest() + + return base64.b64encode(digest).decode("utf-8") + + def _sign_request(self, request: PipelineRequest) -> None: + verb = request.http_request.method.upper() + + # Get the path and query from url, which looks like https://host/path/query + parsed_url = urlparse(request.http_request.url) + query_url = parsed_url.path + + if parsed_url.query: + query_url += "?" + parsed_url.query + + # Need URL() to get a correct encoded key value, from "%3A" to ":", when transport is in type AioHttpTransport. + # There's a similar scenario in azure-storage-blob and azure-appconfiguration, the check logic is from there. + try: + from yarl import URL + from azure.core.pipeline.transport import ( # pylint:disable=non-abstract-transport-import + AioHttpTransport, + ) + + if ( + isinstance(request.context.transport, AioHttpTransport) + or isinstance( + getattr(request.context.transport, "_transport", None), + AioHttpTransport, + ) + or isinstance( + getattr( + getattr(request.context.transport, "_transport", None), + "_transport", + None, + ), + AioHttpTransport, + ) + ): + query_url = str(URL(query_url)) + except (ImportError, TypeError): + pass + + if self._decode_url: + query_url = unquote(query_url) + + signed_headers = "x-ms-date;host;x-ms-content-sha256" + + utc_now = get_current_utc_time() + if request.http_request.body is None: + request.http_request.body = "" + content_digest = hashlib.sha256((request.http_request.body.encode("utf-8"))).digest() + content_hash = base64.b64encode(content_digest).decode("utf-8") + + string_to_sign = verb + "\n" + query_url + "\n" + utc_now + ";" + self._host + ";" + content_hash + + signature = self._compute_hmac(string_to_sign) + + signature_header = { + "x-ms-date": utc_now, + "x-ms-content-sha256": content_hash, + "x-ms-return-client-request-id": "true", + "Authorization": "HMAC-SHA256 SignedHeaders=" + signed_headers + "&Signature=" + signature, + } + + request.http_request.headers.update(signature_header) + + def on_request(self, request: PipelineRequest) -> None: + self._sign_request(request) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py new file mode 100644 index 000000000000..1b0dde140479 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange.py @@ -0,0 +1,78 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import json +from typing import Any, List, Optional + +# pylint: disable=non-abstract-transport-import +# pylint: disable=no-name-in-module + +from azure.core.credentials import AccessToken +from azure.core.pipeline import Pipeline, PipelineResponse +from azure.core.pipeline.policies import BearerTokenCredentialPolicy +from azure.core.exceptions import HttpResponseError +from azure.core.credentials import TokenCredential +from azure.core.pipeline.policies import RetryPolicy +from .entra_token_guard_policy import EntraTokenGuardPolicy +from . import token_utils + + +class TokenExchangeClient: + """Represents a client that exchanges an Entra token for an Azure Communication Services (ACS) token. + + :param resource_endpoint: The endpoint URL of the resource to authenticate against. + :param credential: The credential to use for token exchange. + :param scopes: The scopes to request during the token exchange. + :keyword transport: Optional transport to use for the pipeline. + """ + + # pylint: disable=C4748 + # pylint: disable=client-method-missing-type-annotations + def __init__( + self, resource_endpoint: str, credential: TokenCredential, scopes: Optional[List[str]] = None, **kwargs: Any + ): + + self._resource_endpoint = resource_endpoint + self._scopes = scopes or ["https://communication.azure.com/clients/.default"] + self._credential = credential + pipeline_transport = kwargs.get("transport", None) + self._pipeline = self._create_pipeline_from_options(pipeline_transport) + + def _create_pipeline_from_options(self, pipeline_transport): + auth_policy = BearerTokenCredentialPolicy(self._credential, *self._scopes) + entra_token_guard_policy = EntraTokenGuardPolicy() + retry_policy = RetryPolicy() + policies = [auth_policy, entra_token_guard_policy, retry_policy] + if pipeline_transport: + return Pipeline(policies=policies, transport=pipeline_transport) + + from azure.core.pipeline.transport import RequestsTransport + + return Pipeline(policies=policies, transport=RequestsTransport()) + + def exchange_entra_token(self) -> AccessToken: + message = token_utils.create_request_message(self._resource_endpoint, self._scopes) + response = self._pipeline.run(message) + return self._parse_access_token_from_response(response) + + def _parse_access_token_from_response(self, response: PipelineResponse) -> AccessToken: + if response.http_response.status_code == 200: + try: + content = response.http_response.text() + data = json.loads(content) + access_token_json = data["accessToken"] + token = access_token_json["token"] + expires_on = access_token_json["expiresOn"] + expires_on_epoch = token_utils.parse_expires_on(expires_on, response) + if expires_on_epoch is None: + raise ValueError("Failed to parse 'expiresOn' value from access token response") + return AccessToken(token, expires_on_epoch) + except Exception as ex: + raise ValueError("Failed to parse access token from response") from ex + else: + raise HttpResponseError( + message="Failed to exchange Entra token for ACS token", response=response.http_response + ) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py new file mode 100644 index 000000000000..22dd4582460b --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_exchange_async.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import json +from typing import Any, Optional, List + +# pylint: disable=non-abstract-transport-import +# pylint: disable=no-name-in-module + +from azure.core.credentials import AccessToken +from azure.core.pipeline import AsyncPipeline, PipelineResponse +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy +from azure.core.exceptions import HttpResponseError +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.pipeline.policies import AsyncRetryPolicy +from .entra_token_guard_policy_async import EntraTokenGuardPolicy +from . import token_utils + + +class TokenExchangeClient: + """Asynchronous client that exchanges an Entra token for an Azure Communication Services (ACS) token. + + :param resource_endpoint: The endpoint URL of the resource to authenticate against. + :param credential: The credential to use for token exchange. + :param scopes: The scopes to request during the token exchange. + :keyword transport: Optional transport to use for the pipeline. + """ + + # pylint: disable=C4748 + # pylint: disable=client-method-missing-type-annotations + def __init__( + self, + resource_endpoint: str, + credential: AsyncTokenCredential, + scopes: Optional[List[str]] = None, + **kwargs: Any + ): + + self._resource_endpoint = resource_endpoint + self._scopes = scopes or ["https://communication.azure.com/clients/.default"] + self._credential = credential + pipeline_transport = kwargs.get("transport", None) + self._pipeline = self._create_pipeline_from_options(pipeline_transport) + + def _create_pipeline_from_options(self, pipeline_transport): + auth_policy = AsyncBearerTokenCredentialPolicy(self._credential, *self._scopes) + entra_token_guard_policy = EntraTokenGuardPolicy() + retry_policy = AsyncRetryPolicy() + policies = [auth_policy, entra_token_guard_policy, retry_policy] + if pipeline_transport: + return AsyncPipeline(policies=policies, transport=pipeline_transport) + + from azure.core.pipeline.transport import AioHttpTransport + + return AsyncPipeline(policies=policies, transport=AioHttpTransport()) + + async def exchange_entra_token(self) -> AccessToken: + message = token_utils.create_request_message(self._resource_endpoint, self._scopes) + response = await self._pipeline.run(message) + return await self._parse_access_token_from_response(response) + + async def _parse_access_token_from_response(self, response: PipelineResponse) -> AccessToken: + if response.http_response.status_code == 200: + try: + content = response.http_response.text() + data = json.loads(content) + access_token_json = data["accessToken"] + token = access_token_json["token"] + expires_on = access_token_json["expiresOn"] + expires_on_epoch = token_utils.parse_expires_on(expires_on, response) + if expires_on_epoch is None: + raise ValueError("Failed to parse 'expiresOn' value from access token response") + return AccessToken(token, expires_on_epoch) + except Exception as ex: + raise ValueError("Failed to parse access token from response") from ex + else: + raise HttpResponseError( + message="Failed to exchange Entra token for ACS token", response=response.http_response + ) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py new file mode 100644 index 000000000000..9628d3e76ec9 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/token_utils.py @@ -0,0 +1,91 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + + +import json +from datetime import datetime, timezone +from typing import Tuple, Any, List, Optional +import isodate +from azure.core.exceptions import HttpResponseError + +TEAMS_EXTENSION_SCOPE_PREFIX = "https://auth.msft.communication.azure.com/" +COMMUNICATION_CLIENTS_SCOPE_PREFIX = "https://communication.azure.com/clients/" +TEAMS_EXTENSION_ENDPOINT = "/access/teamsExtension/:exchangeAccessToken" +TEAMS_EXTENSION_API_VERSION = "2025-06-30" +COMMUNICATION_CLIENTS_ENDPOINT = "/access/entra/:exchangeAccessToken" +COMMUNICATION_CLIENTS_API_VERSION = "2025-03-02-preview" + + +def create_request_message(resource_endpoint: str, scopes: Optional[List[str]]) -> Any: + from azure.core.pipeline.transport import HttpRequest + + request_uri = create_request_uri(resource_endpoint, scopes) + request = HttpRequest("POST", request_uri) + request.headers["Accept"] = "application/json" + request.headers["Content-Type"] = "application/json" + request.set_json_body({}) + return request + + +def create_request_uri(resource_endpoint: str, scopes: Optional[List[str]]) -> str: + endpoint, api_version = determine_endpoint_and_api_version(scopes) + base = resource_endpoint.rstrip("/") + return f"{base}{endpoint}?api-version={api_version}" + + +def determine_endpoint_and_api_version(scopes: Optional[List[str]]) -> Tuple[str, str]: + if not scopes or not isinstance(scopes, list): + raise ValueError( + "Scopes validation failed. Ensure all scopes start with either " + f"{TEAMS_EXTENSION_SCOPE_PREFIX} or {COMMUNICATION_CLIENTS_SCOPE_PREFIX}." + ) + if all(scope.startswith(TEAMS_EXTENSION_SCOPE_PREFIX) for scope in scopes): + return TEAMS_EXTENSION_ENDPOINT, TEAMS_EXTENSION_API_VERSION + if all(scope.startswith(COMMUNICATION_CLIENTS_SCOPE_PREFIX) for scope in scopes): + return COMMUNICATION_CLIENTS_ENDPOINT, COMMUNICATION_CLIENTS_API_VERSION + raise ValueError( + "Scopes validation failed. Ensure all scopes start with either" + f"{TEAMS_EXTENSION_SCOPE_PREFIX} or {COMMUNICATION_CLIENTS_SCOPE_PREFIX}." + ) + + +def parse_expires_on(expires_on, response): + if isinstance(expires_on, str): + try: + expires_on_dt = isodate.parse_datetime(expires_on) + expires_on_epoch = int(expires_on_dt.timestamp()) + return expires_on_epoch + except Exception as exc: + raise HttpResponseError( + message="Unknown format for expires_on field in access token response", response=response.http_response + ) from exc + else: + raise HttpResponseError( + message="Missing expires_on field in access token response", response=response.http_response + ) + + +def is_entra_token_cache_valid(entra_token_cache, request): + current_entra_token = request.http_request.headers.get("Authorization", "") + cache_valid = entra_token_cache is not None and current_entra_token == entra_token_cache + return cache_valid, current_entra_token + + +def is_acs_token_cache_valid(response_cache): + if ( + response_cache is None + or response_cache.http_response is None + or response_cache.http_response.status_code != 200 + ): + return False + try: + content = response_cache.http_response.text() + data = json.loads(content) + expires_on = data["accessToken"]["expiresOn"] + expires_on_dt = isodate.parse_datetime(expires_on) + return datetime.now(timezone.utc) < expires_on_dt + except (KeyError, ValueError, json.JSONDecodeError) as e: + raise ValueError("Invalid token response") from e diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py new file mode 100644 index 000000000000..17adbf77dcc9 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -0,0 +1,213 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event +from datetime import timedelta +from typing import Any, Optional, overload, Callable +from azure.core.credentials import TokenCredential, AccessToken +from .utils import get_current_utc_as_int +from .utils import create_access_token +from .token_exchange import TokenExchangeClient + + +class CommunicationTokenCredential(object): + """Credential type used for authenticating to an Azure Communication service. + + :param str token: The token used to authenticate to an Azure Communication service. + :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], AccessToken] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires. + The proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. + :keyword str resource_endpoint: The endpoint URL of the resource to authenticate against. + :keyword token_credential: The credential to use for token exchange. + :paramtype token_credential: ~azure.core.credentials.TokenCredential + :keyword list[str] scopes: The scopes to request during the token exchange. If not provided, + a default value will be used: https://communication.azure.com/clients/.default + + :raises: TypeError if parameter 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. + """ + + _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + + @overload + def __init__( + self, + token: str, + *, + token_refresher: Optional[Callable[[], AccessToken]] = None, + proactive_refresh: bool = False, + **kwargs: Any, + ): + """ + Initializes the CommunicationTokenCredential. + + :param str token: The token used to authenticate to an Azure Communication service. + :param token_refresher: Optional callable to refresh the token. + :param proactive_refresh: Whether to refresh the token proactively. + :param kwargs: Additional keyword arguments. + """ + + @overload + def __init__( + self, + *, + resource_endpoint: str, + token_credential: TokenCredential, + scopes: Optional[list[str]] = None, + **kwargs: Any, + ): + """ + Initializes the CommunicationTokenCredential using token exchange. + + :param resource_endpoint: The endpoint URL of the resource to authenticate against. + :param token_credential: The credential to use for token exchange. + :param scopes: The scopes to request during the token exchange. + :param kwargs: Additional keyword arguments. + """ + + def __init__(self, token: Optional[str] = None, **kwargs: Any): + resource_endpoint = kwargs.pop("resource_endpoint", None) + token_credential = kwargs.pop("token_credential", None) + scopes = kwargs.pop("scopes", None) + + # Check if at least one field exists but not all fields exist when token is None + fields_present = [resource_endpoint, token_credential] + fields_exist = [field is not None for field in fields_present] + + if token is None and not all(fields_exist): + missing_fields = [] + if resource_endpoint is None: + missing_fields.append("resource_endpoint") + if token_credential is None: + missing_fields.append("token_credential") + raise ValueError( + "When using token exchange, resource_endpoint and token_credential must be provided. " + f"Missing: {', '.join(missing_fields)}" + ) + + self._token_exchange_client = None + if resource_endpoint and token_credential: + self._token_exchange_client = TokenExchangeClient(resource_endpoint, token_credential, scopes) + self._token_refresher = self._token_exchange_client.exchange_entra_token + self._proactive_refresh = False + self._token = self._token_exchange_client.exchange_entra_token() + else: + if not isinstance(token, str): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop("token_refresher", None) + self._proactive_refresh = kwargs.pop("proactive_refresh", False) + if self._proactive_refresh and self._token_refresher is None: + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") + self._timer = None + self._lock = Condition(Lock()) + self._some_thread_refreshing = False + self._is_closed = Event() + + def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken + """The value of the configured token. + :param any scopes: Scopes to be added to the token. + :return: AccessToken + :rtype: ~azure.core.credentials.AccessToken + """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + + if not self._token_refresher or not self._is_token_expiring_soon(self._token): + return self._token + self._update_token_and_reschedule() + return self._token + + def _update_token_and_reschedule(self): + should_this_thread_refresh = False + with self._lock: + while self._is_token_expiring_soon(self._token): + if self._some_thread_refreshing: + if self._is_token_valid(self._token): + return self._token + self._wait_till_lock_owner_finishes_refreshing() + else: + should_this_thread_refresh = True + self._some_thread_refreshing = True + break + + if should_this_thread_refresh: + try: + new_token = self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError("The token returned from the token_refresher is expired.") + with self._lock: + self._token = new_token + self._some_thread_refreshing = False + self._lock.notify_all() + except: + with self._lock: + self._some_thread_refreshing = False + self._lock.notify_all() + raise + if self._proactive_refresh: + self._schedule_refresh() + return self._token + + def _schedule_refresh(self): + if self._is_closed.is_set(): + return + if self._timer is not None: + self._timer.cancel() + + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl // 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + if timespan <= TIMEOUT_MAX: + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.daemon = True + self._timer.start() + + def _wait_till_lock_owner_finishes_refreshing(self): + self._lock.release() + self._lock.acquire() + + def _is_token_expiring_soon(self, token): + if self._proactive_refresh: + interval = timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) + else: + interval = timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) + return (token.expires_on - get_current_utc_as_int()) < interval.total_seconds() + + @classmethod + def _is_token_valid(cls, token): + return get_current_utc_as_int() < token.expires_on + + def __enter__(self): + if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed." + ) + self._schedule_refresh() + return self + + def __exit__(self, *args): + self.close() + + def close(self) -> None: + if self._timer is not None: + self._timer.cancel() + self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py new file mode 100644 index 000000000000..81c4b79c4e6d --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -0,0 +1,225 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# pylint: disable=C4763 + +import asyncio +import sys +from asyncio import Condition, Lock, Event +from datetime import timedelta +from typing import Any, Optional, overload, Awaitable, Callable +from azure.core.credentials_async import AsyncTokenCredential +from .utils import get_current_utc_as_int +from .utils import create_access_token +from .utils_async import AsyncTimer +from .token_exchange_async import TokenExchangeClient + + +class CommunicationTokenCredential(object): + """Credential type used for authenticating to an Azure Communication service. + + :param str token: The token used to authenticate to an Azure Communication service. + :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token + expires. The proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context + manager or the 'close' method must be called once the object usage has been finished. + :keyword str resource_endpoint: The endpoint URL of the resource to authenticate against. + :keyword token_credential: The credential to use for token exchange. + :paramtype token_credential: ~azure.core.credentials.AsyncTokenCredential + :keyword list[str] scopes: The scopes to request during the token exchange. If not provided, + a default value will be used: https://communication.azure.com/clients/.default + + :raises: TypeError if parameter 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. + """ + + _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + + @overload + def __init__( + self, + token: str, + *, + token_refresher: Optional[Callable[[], Awaitable[Any]]] = None, + proactive_refresh: bool = False, + **kwargs: Any, + ): + """ + Initializes the CommunicationTokenCredential. + + :param str token: The token used to authenticate to an Azure Communication service. + :param token_refresher: Optional async callable to refresh the token. + :param proactive_refresh: Whether to refresh the token proactively. + :param kwargs: Additional keyword arguments. + """ + + @overload + def __init__( + self, + *, + resource_endpoint: str, + token_credential: AsyncTokenCredential, + scopes: Optional[list[str]] = None, + **kwargs: Any, + ): + """ + Initializes the CommunicationTokenCredential using token exchange. + + :param resource_endpoint: The endpoint URL of the resource to authenticate against. + :param token_credential: The credential to use for token exchange. + :param scopes: The scopes to request during the token exchange. + :param kwargs: Additional keyword arguments. + """ + + def __init__(self, token: Optional[str] = None, **kwargs: Any): + resource_endpoint = kwargs.pop("resource_endpoint", None) + token_credential = kwargs.pop("token_credential", None) + scopes = kwargs.pop("scopes", None) + + # Check if at least one field exists but not all fields exist when token is None + fields_present = [resource_endpoint, token_credential] + fields_exist = [field is not None for field in fields_present] + + if token is None and not all(fields_exist): + missing_fields = [] + if resource_endpoint is None: + missing_fields.append("resource_endpoint") + if token_credential is None: + missing_fields.append("token_credential") + raise ValueError( + "When using token exchange, resource_endpoint and token_credential must be provided. " + f"Missing: {', '.join(missing_fields)}" + ) + + self._token_exchange_client = None + if resource_endpoint and token_credential: + self._token_exchange_client = TokenExchangeClient(resource_endpoint, token_credential, scopes) + self._token_refresher = self._token_exchange_client.exchange_entra_token + self._proactive_refresh = False + + async def _initialize_token(): + self._token = await self._token_exchange_client.exchange_entra_token() + + asyncio.get_event_loop().run_until_complete(_initialize_token()) + else: + if not isinstance(token, str): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop("token_refresher", None) + self._proactive_refresh = kwargs.pop("proactive_refresh", False) + if self._proactive_refresh and self._token_refresher is None: + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") + + self._timer = None + self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, "_get_loop", lambda: None)() + self._lock = Condition(self._async_mutex) + self._some_thread_refreshing = False + self._is_closed = Event() + + async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken + """The value of the configured token. + :param any scopes: Scopes to be added to the token. + :return: AccessToken + :rtype: ~azure.core.credentials.AccessToken + """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + + if not self._token_refresher or not self._is_token_expiring_soon(self._token): + return self._token + await self._update_token_and_reschedule() + return self._token + + async def _update_token_and_reschedule(self): + should_this_thread_refresh = False + async with self._lock: + while self._is_token_expiring_soon(self._token): + if self._some_thread_refreshing: + if self._is_token_valid(self._token): + return self._token + await self._wait_till_lock_owner_finishes_refreshing() + else: + should_this_thread_refresh = True + self._some_thread_refreshing = True + break + + if should_this_thread_refresh: + try: + new_token = await self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError("The token returned from the token_refresher is expired.") + async with self._lock: + self._token = new_token + self._some_thread_refreshing = False + self._lock.notify_all() + except: + async with self._lock: + self._some_thread_refreshing = False + self._lock.notify_all() + raise + if self._proactive_refresh: + self._schedule_refresh() + return self._token + + def _schedule_refresh(self): + if self._is_closed.is_set(): + return + if self._timer is not None: + self._timer.cancel() + + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl // 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) + self._timer.start() + + async def _wait_till_lock_owner_finishes_refreshing(self): + self._lock.release() + await self._lock.acquire() + + def _is_token_expiring_soon(self, token): + if self._proactive_refresh: + interval = timedelta(minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) + else: + interval = timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) + return (token.expires_on - get_current_utc_as_int()) < interval.total_seconds() + + @classmethod + def _is_token_valid(cls, token): + return get_current_utc_as_int() < token.expires_on + + async def __aenter__(self): + if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed." + ) + self._schedule_refresh() + return self + + async def __aexit__(self, *args): + await self.close() + + async def close(self) -> None: + if self._timer is not None: + self._timer.cancel() + self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py new file mode 100644 index 000000000000..38a62a49dc3f --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -0,0 +1,89 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import base64 +import json +import calendar +from typing import cast, Tuple, Optional +from datetime import datetime +from azure.core.serialization import TZ_UTC +from azure.core.credentials import AccessToken + + +def _convert_datetime_to_utc_int(input_datetime) -> int: + """ + Converts DateTime in local time to the Epoch in UTC in second. + + :param input_datetime: Input datetime + :type input_datetime: datetime + :return: Integer + :rtype: int + """ + return int(calendar.timegm(input_datetime.utctimetuple())) + + +def parse_connection_str(conn_str: Optional[str]) -> Tuple[str, str]: + if conn_str is None: + raise ValueError("Connection string is undefined.") + endpoint = None + shared_access_key = None + for element in conn_str.split(";"): + key, _, value = element.partition("=") + if key.lower() == "endpoint": + endpoint = value.rstrip("/") + elif key.lower() == "accesskey": + shared_access_key = value + if not all([endpoint, shared_access_key]): + raise ValueError( + "Invalid connection string. You can get the connection string from your resource page in the Azure Portal. " + "The format should be as follows: endpoint=https:///;accesskey=" + ) + left_slash_pos = cast(str, endpoint).find("//") + if left_slash_pos != -1: + host = cast(str, endpoint)[left_slash_pos + 2 :] + else: + host = str(endpoint) + + return host, str(shared_access_key) + + +def get_current_utc_time() -> str: + return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + + +def get_current_utc_as_int() -> int: + current_utc_datetime = datetime.utcnow() + return _convert_datetime_to_utc_int(current_utc_datetime) + + +def create_access_token(token) -> AccessToken: + """Creates an instance of azure.core.credentials.AccessToken from a + string token. The input string is jwt token in the following form: + .. + This method looks into the token_payload which is a json and extracts the expiry time + for that token and creates a tuple of type azure.core.credentials.AccessToken + (, ) + :param token: User token + :type token: str + :return: Instance of azure.core.credentials.AccessToken - token and expiry date of it + :rtype: ~azure.core.credentials.AccessToken + """ + + token_parse_err_msg = "Token is not formatted correctly" + parts = token.split(".") + + if len(parts) < 3: + raise ValueError(token_parse_err_msg) + + try: + padded_base64_payload = base64.b64decode(parts[1] + "==").decode("ascii") + payload = json.loads(padded_base64_payload) + return AccessToken( + token, + _convert_datetime_to_utc_int(datetime.fromtimestamp(payload["exp"], TZ_UTC)), + ) + except ValueError as val_error: + raise ValueError(token_parse_err_msg) from val_error diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py new file mode 100644 index 000000000000..b779d5c814b3 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +# pylint: disable=C4763 +from asyncio import sleep, ensure_future + + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = ensure_future(self._job()) + + async def _job(self): + await sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() + self._task = None diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py new file mode 100644 index 000000000000..2069daa52a02 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/__init__.py @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from ._communication_identity_client_async import CommunicationIdentityClient + +__all__ = ["CommunicationIdentityClient"] From c299e8322b360d47bb524ade66bef3c8b2dfef8d Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 09:58:51 -0700 Subject: [PATCH 08/16] ACS Identity: drop the enum alias made obsolete by client.tsp client.tsp renames the token scope union for Python, so the generated enum is CommunicationTokenScope directly and the compensating alias import now refers to a name that no longer exists. Hand-written change only; the generated layer is left exactly as the pipeline emitted it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure/communication/identity/__init__.py | 2 +- .../_communication_identity_client.py | 52 +++++++++---------- .../_communication_identity_client_async.py | 32 +++--------- 3 files changed, 34 insertions(+), 52 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py index c23199142080..c7ad2d71dfa7 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/__init__.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- from ._communication_identity_client import CommunicationIdentityClient -from ._generated.models import CommunicationIdentityTokenScope as CommunicationTokenScope +from ._generated.models import CommunicationTokenScope from ._shared.models import ( CommunicationCloudEnvironment, diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py index 2cfc1a97648c..894110b05837 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -16,14 +16,7 @@ from ._shared.models import CommunicationUserIdentifier from ._version import SDK_MONIKER from ._api_versions import DEFAULT_VERSION -from ._utils import ( - _ACCEPT_JSON, - BodylessCreateContentTypePolicy, - build_token_request_body, - extract_create_content_type, - merge_headers, - to_access_token, -) +from ._utils import build_token_request_body, to_access_token from ._generated.models import CommunicationTokenScope if TYPE_CHECKING: @@ -63,19 +56,20 @@ def __init__( raise ValueError("You need to provide account shared key to authenticate.") self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) - per_call_policies = list(kwargs.pop("per_call_policies", None) or []) - per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential), sdk_moniker=SDK_MONIKER, - per_call_policies=per_call_policies, **kwargs ) @classmethod - def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "CommunicationIdentityClient": + def from_connection_string( + cls, + conn_str: str, + **kwargs: Any + ) -> "CommunicationIdentityClient": """Create CommunicationIdentityClient from a Connection String. :param str conn_str: A connection string to an Azure Communication Service resource. @@ -95,17 +89,14 @@ def create_user(self, **kwargs) -> CommunicationUserIdentifier: :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = self._identity_service_client.identity_operations.create( - **extract_create_content_type(kwargs) - ) + identity_access_token = self._identity_service_client.identity_operations.create(**kwargs) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace def create_user_and_token( self, - scopes: List[Union[str, CommunicationTokenScope]], - *, + scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> Tuple[CommunicationUserIdentifier, AccessToken]: @@ -133,7 +124,11 @@ def create_user_and_token( return user_identifier, access_token @distributed_trace - def delete_user(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: + def delete_user( + self, + user: CommunicationUserIdentifier, + **kwargs: Any + ) -> None: """Triggers revocation event for user and deletes all its data. :param user: Azure Communication User to delete @@ -141,16 +136,13 @@ def delete_user(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: :return: None :rtype: None """ - self._identity_service_client.identity_operations.delete( - user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) - ) + self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) @distributed_trace def get_token( self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], - *, + scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> AccessToken: @@ -175,7 +167,11 @@ def get_token( return to_access_token(access_token) @distributed_trace - def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: + def revoke_tokens( + self, + user: CommunicationUserIdentifier, + **kwargs: Any + ) -> None: """Schedule revocation of all tokens of an identity. :param user: Azure Communication User. @@ -184,12 +180,16 @@ def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs: Any) -> Non :rtype: None """ return self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore + user.properties["id"] if user else None, **kwargs # type: ignore ) @distributed_trace def get_token_for_teams_user( - self, aad_token: str, client_id: str, user_object_id: str, **kwargs: Any + self, + aad_token: str, + client_id: str, + user_object_id: str, + **kwargs: Any ) -> AccessToken: """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py index 6fdc6bc78932..4fb2d73913db 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -18,14 +18,7 @@ from .._shared.models import CommunicationUserIdentifier from .._version import SDK_MONIKER from .._api_versions import DEFAULT_VERSION -from .._utils import ( - _ACCEPT_JSON, - BodylessCreateContentTypePolicy, - build_token_request_body, - extract_create_content_type, - merge_headers, - to_access_token, -) +from .._utils import build_token_request_body, to_access_token from .._generated.models import CommunicationTokenScope @@ -58,14 +51,11 @@ def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureK self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) - per_call_policies = list(kwargs.pop("per_call_policies", None) or []) - per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential, decode_url=True, is_async=True), sdk_moniker=SDK_MONIKER, - per_call_policies=per_call_policies, **kwargs ) @@ -91,17 +81,13 @@ async def create_user(self, **kwargs) -> "CommunicationUserIdentifier": :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = await self._identity_service_client.identity_operations.create( - **extract_create_content_type(kwargs) - ) + identity_access_token = await self._identity_service_client.identity_operations.create(**kwargs) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace_async async def create_user_and_token( - self, - scopes: List[Union[str, CommunicationTokenScope]], - *, + self, scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> Tuple["CommunicationUserIdentifier", AccessToken]: @@ -139,16 +125,12 @@ async def delete_user(self, user: CommunicationUserIdentifier, **kwargs) -> None :return: None :rtype: None """ - await self._identity_service_client.identity_operations.delete( - user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) - ) + await self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) @distributed_trace_async async def get_token( - self, - user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], - *, + self, user: CommunicationUserIdentifier, + scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> AccessToken: @@ -182,7 +164,7 @@ async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> No :rtype: None """ return await self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore + user.properties["id"] if user else None, **kwargs # type: ignore ) @distributed_trace_async From d73bafead8d1fa466518efe89fc60d4f3f13650e Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 10:00:06 -0700 Subject: [PATCH 09/16] ACS Identity: reapply header restoration lost in conflict resolution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../_communication_identity_client.py | 52 +++++++++---------- .../_communication_identity_client_async.py | 32 +++++++++--- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py index 894110b05837..2cfc1a97648c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -16,7 +16,14 @@ from ._shared.models import CommunicationUserIdentifier from ._version import SDK_MONIKER from ._api_versions import DEFAULT_VERSION -from ._utils import build_token_request_body, to_access_token +from ._utils import ( + _ACCEPT_JSON, + BodylessCreateContentTypePolicy, + build_token_request_body, + extract_create_content_type, + merge_headers, + to_access_token, +) from ._generated.models import CommunicationTokenScope if TYPE_CHECKING: @@ -56,20 +63,19 @@ def __init__( raise ValueError("You need to provide account shared key to authenticate.") self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) + per_call_policies = list(kwargs.pop("per_call_policies", None) or []) + per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential), sdk_moniker=SDK_MONIKER, + per_call_policies=per_call_policies, **kwargs ) @classmethod - def from_connection_string( - cls, - conn_str: str, - **kwargs: Any - ) -> "CommunicationIdentityClient": + def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "CommunicationIdentityClient": """Create CommunicationIdentityClient from a Connection String. :param str conn_str: A connection string to an Azure Communication Service resource. @@ -89,14 +95,17 @@ def create_user(self, **kwargs) -> CommunicationUserIdentifier: :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = self._identity_service_client.identity_operations.create(**kwargs) + identity_access_token = self._identity_service_client.identity_operations.create( + **extract_create_content_type(kwargs) + ) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace def create_user_and_token( self, - scopes: List[Union[str, CommunicationTokenScope]], *, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> Tuple[CommunicationUserIdentifier, AccessToken]: @@ -124,11 +133,7 @@ def create_user_and_token( return user_identifier, access_token @distributed_trace - def delete_user( - self, - user: CommunicationUserIdentifier, - **kwargs: Any - ) -> None: + def delete_user(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: """Triggers revocation event for user and deletes all its data. :param user: Azure Communication User to delete @@ -136,13 +141,16 @@ def delete_user( :return: None :rtype: None """ - self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) + self._identity_service_client.identity_operations.delete( + user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) + ) @distributed_trace def get_token( self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], *, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> AccessToken: @@ -167,11 +175,7 @@ def get_token( return to_access_token(access_token) @distributed_trace - def revoke_tokens( - self, - user: CommunicationUserIdentifier, - **kwargs: Any - ) -> None: + def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: """Schedule revocation of all tokens of an identity. :param user: Azure Communication User. @@ -180,16 +184,12 @@ def revoke_tokens( :rtype: None """ return self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **kwargs # type: ignore + user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore ) @distributed_trace def get_token_for_teams_user( - self, - aad_token: str, - client_id: str, - user_object_id: str, - **kwargs: Any + self, aad_token: str, client_id: str, user_object_id: str, **kwargs: Any ) -> AccessToken: """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py index 4fb2d73913db..6fdc6bc78932 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -18,7 +18,14 @@ from .._shared.models import CommunicationUserIdentifier from .._version import SDK_MONIKER from .._api_versions import DEFAULT_VERSION -from .._utils import build_token_request_body, to_access_token +from .._utils import ( + _ACCEPT_JSON, + BodylessCreateContentTypePolicy, + build_token_request_body, + extract_create_content_type, + merge_headers, + to_access_token, +) from .._generated.models import CommunicationTokenScope @@ -51,11 +58,14 @@ def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureK self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) + per_call_policies = list(kwargs.pop("per_call_policies", None) or []) + per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential, decode_url=True, is_async=True), sdk_moniker=SDK_MONIKER, + per_call_policies=per_call_policies, **kwargs ) @@ -81,13 +91,17 @@ async def create_user(self, **kwargs) -> "CommunicationUserIdentifier": :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = await self._identity_service_client.identity_operations.create(**kwargs) + identity_access_token = await self._identity_service_client.identity_operations.create( + **extract_create_content_type(kwargs) + ) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace_async async def create_user_and_token( - self, scopes: List[Union[str, CommunicationTokenScope]], *, + self, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> Tuple["CommunicationUserIdentifier", AccessToken]: @@ -125,12 +139,16 @@ async def delete_user(self, user: CommunicationUserIdentifier, **kwargs) -> None :return: None :rtype: None """ - await self._identity_service_client.identity_operations.delete(user.properties["id"], **kwargs) + await self._identity_service_client.identity_operations.delete( + user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) + ) @distributed_trace_async async def get_token( - self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], *, + self, + user: CommunicationUserIdentifier, + scopes: List[Union[str, CommunicationTokenScope]], + *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> AccessToken: @@ -164,7 +182,7 @@ async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> No :rtype: None """ return await self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **kwargs # type: ignore + user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore ) @distributed_trace_async From fa08a48eb884b382794811017e5c76a14da80a0a Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 10:13:40 -0700 Subject: [PATCH 10/16] ACS Identity: fix development status classifier for the GA release The pipeline-generated pyproject.toml classifies the distribution as "Development Status :: 4 - Beta" while the package ships as 2.0.0, a stable GA release. The repo's own gate rejects that combination: ci_tools.functions.verify_package_classifiers( "azure-communication-identity", "2.0.0", ["Development Status :: 4 - Beta"]) -> False, "has version 2.0.0 and is a GA release, but had development status 'Development Status :: 4 - Beta'. Expecting a development classifier that is equal or greater than 'Development Status :: 5 - Production/Stable'." setup.py does already carry "Development Status :: 5 - Production/Stable", but that has no effect here and is not a mitigation. pyproject.toml declares a [project] table, so under PEP 621 it is authoritative for package metadata and setup.py's classifiers are ignored entirely. Building the sdist before this change produced: Version: 2.0.0 Classifier: Development Status :: 4 - Beta and after it produces "Development Status :: 5 - Production/Stable". The gate function returns True for the corrected value. pyproject.toml is emitted by the generation pipeline, so this recurs on every regeneration until the emitter or its configuration is corrected upstream. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sdk/communication/azure-communication-identity/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-identity/pyproject.toml b/sdk/communication/azure-communication-identity/pyproject.toml index 5118e829b7ef..d85b497d8daa 100644 --- a/sdk/communication/azure-communication-identity/pyproject.toml +++ b/sdk/communication/azure-communication-identity/pyproject.toml @@ -13,7 +13,7 @@ authors = [ description = "Microsoft Corporation Azure Communication Identity Client Library for Python" license = "MIT" classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", From f9b13647aeabf0449fb50ebdff62faec5202b2c6 Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 11:13:42 -0700 Subject: [PATCH 11/16] ACS Identity: sanitize api-version so recorded tests match The recorded sessions were captured against api-version 2023-10-01. This package now defaults to 2026-09-23, so every recorded request failed to match its cassette: ResourceNotFoundError: Playback failure Unable to find a record for the request ... Uri doesn't match Measured, not assumed: the assets tag python/communication/azure-communication-identity_b8abdd90a9 was cloned and its 92 cassettes scanned. 2023-10-01 is the only api-version present, in 71 occurrences. Adds a URI regex sanitizer mapping the current version back to the recorded one. The two versions are functionally identical for these operations, so the recordings remain valid and only the query string differs. This retires itself: once the cassettes are re-recorded at 2026-09-23 the regex matches nothing. ignored_query_parameters was deliberately not used. It substitutes a "Volatile" literal on both sides, so any api-version would match any recording - a blanket hole in the single field this release changes. The regex maps one exact version to one other exact version, so every other value still fails to match. Known limitation: sanitizers apply to both the recording and the request, so the two versions become mutually interchangeable and a regression back to 2023-10-01 would not be caught here. tests/test_wire_contract.py asserts the exact api-version against a literal, which covers that gap. Verified by playback against the real cassettes, not by inspection: with the sanitizer 49 passed bogus target version (no match) 34 failed bogus replacement (rewrites to nothing) 34 failed sanitizer removed entirely 34 failed restored 49 passed The 15 that pass in every case are client-side validation and Teams-user tests that raise before any HTTP request is issued, so no cassette is consulted. Full suite: 212 passed, matching the 163 passed / 49 failed reported by CI at build 6811270. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/conftest.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sdk/communication/azure-communication-identity/tests/conftest.py b/sdk/communication/azure-communication-identity/tests/conftest.py index 3e8e7cbfbbae..c0c11bca8276 100644 --- a/sdk/communication/azure-communication-identity/tests/conftest.py +++ b/sdk/communication/azure-communication-identity/tests/conftest.py @@ -33,6 +33,7 @@ test_proxy, add_general_regex_sanitizer, add_header_regex_sanitizer, + add_uri_regex_sanitizer, set_default_session_settings, add_body_key_sanitizer, add_oauth_response_sanitizer, @@ -40,6 +41,29 @@ ) from azure.communication.identity._shared.utils import parse_connection_str +# The recorded sessions were captured against api-version 2023-10-01. This package now +# defaults to 2026-09-23, so the request URI no longer matches the recording and every +# recorded test fails with "Unable to find a record for the request". +# +# The two versions are functionally identical for these operations, so the recordings remain +# valid; only the query string differs. Rewriting the request back to the recorded value lets +# them match. This is deliberately narrow -- it maps one exact version to one other exact +# version, so any other api-version still fails to match. +# +# Do NOT replace this with `ignored_query_parameters`: that substitutes a "Volatile" literal +# on both sides, so *any* api-version would match *any* recording, removing all coverage of +# the single field this release changes. +# +# Known limitation: sanitizers apply to both the recording and the incoming request, so the +# two versions become mutually interchangeable and a regression back to 2023-10-01 would not +# be caught here. tests/test_wire_contract.py asserts the exact api-version on the wire +# against a literal, which covers that gap. +# +# This retires itself: once the cassettes are re-recorded at 2026-09-23 the regex matches +# nothing and the sanitizer becomes inert. +RECORDED_API_VERSION = "2023-10-01" +CURRENT_API_VERSION = "2026-09-23" + @pytest.fixture(scope="session", autouse=True) def add_sanitizers(test_proxy): @@ -53,6 +77,11 @@ def add_sanitizers(test_proxy): add_oauth_response_sanitizer() + add_uri_regex_sanitizer( + regex=f"api-version={CURRENT_API_VERSION}", + value=f"api-version={RECORDED_API_VERSION}", + ) + connection_str = os.environ.get("COMMUNICATION_LIVETEST_DYNAMIC_CONNECTION_STRING") if connection_str is not None: endpoint, _ = parse_connection_str(connection_str) From 199625ed975a6967e693d36600f323ade5caf8aa Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 20:59:00 -0700 Subject: [PATCH 12/16] ACS Identity: omit scopes from the request body when None Completes the msrest-parity fix started for expiresInMinutes. The old layer dropped None fields during serialization; the generated code forwards the body as-is. build_token_request_body already omitted expiresInMinutes when unset but still set scopes unconditionally, so a None scope list reached the wire as an explicit null. Measured against the shipped package at 7b1196e742: OLD (AutoRest) NEW (before fix) get_token(user, None) {} {"scopes": null} create_user_and_token(None) {} {"createTokenWithScopes": null} get_token(user, []) {"scopes": []} {"scopes": []} create_user_and_token([]) {"createTokenWithScopes": []} {"createTokenWithScopes": []} After the fix all four match the old client exactly. An empty list is a value rather than an absence and is still sent. This restores prior behaviour and deliberately adds no validation. Checked across eight null-argument cases that exception behaviour is unchanged: the AutoRest client did not raise on a None scope list, and neither does this. The request is still issued and the service still rejects it. Found while checking whether the Python emitter dropped argument null guards the way the C# emitter did. It did not - Python parity is 8/8 on exceptions and was never guarded in the first place - but the check surfaced this body-level difference, which no request-comparison test would have caught because the existing tests only covered the expiry field. Adds three regression tests. Reverting the fix fails two of them. 215 offline tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure/communication/identity/_utils.py | 23 +++++++++++---- .../tests/test_wire_contract.py | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py index 10cd1220fc29..e0ea4af18d5f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py @@ -63,12 +63,21 @@ def build_token_request_body( scopes: List[Union[str, Any]], token_expires_in: Optional[timedelta], ) -> Dict[str, Any]: - """Build a token request body, omitting the expiry when it was not requested. + """Build a token request body, omitting fields the caller did not supply. - ``expiresInMinutes`` is only included when the caller supplied a value. The previous - msrest-based layer dropped ``None`` fields during serialization, whereas the generated - code now forwards the request body as-is, which would put an explicit ``null`` on the - wire for a property the service constrains to [60, 1440]. + The previous msrest-based layer dropped ``None`` fields during serialization, whereas the + generated code forwards the request body as-is. Both ``scopes`` and ``expiresInMinutes`` + are therefore omitted when unset, so the request stays byte-identical to the previously + published SDK. + + ``expiresInMinutes`` matters because the service constrains it to [60, 1440] and an + explicit ``null`` is not a valid value. ``scopes`` is omitted for the same reason the old + layer omitted it -- passing ``None`` for a required argument is a caller error either way, + but it must fail the way it always failed, with the same request on the wire. + + Note this restores prior behaviour rather than adding validation: the AutoRest client did + not raise on a ``None`` scope list, and neither does this. The request is still sent and + the service still rejects it. :param scopes_key: Name of the scopes property expected by the service. :type scopes_key: str @@ -79,7 +88,9 @@ def build_token_request_body( :return: The request body to send to the service. :rtype: dict[str, any] """ - request_body: Dict[str, Any] = {scopes_key: scopes} + request_body: Dict[str, Any] = {} + if scopes is not None: + request_body[scopes_key] = scopes expires_in_minutes = convert_timedelta_to_mins(token_expires_in) if expires_in_minutes is not None: request_body["expiresInMinutes"] = expires_in_minutes diff --git a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py index f390ce948b7f..5d28edaad411 100644 --- a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py +++ b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py @@ -150,6 +150,35 @@ def test_get_token_sends_expiry_when_requested(self, client, transport): ) assert _sent_body(transport)["expiresInMinutes"] == 180 + def test_none_scopes_are_omitted_not_sent_as_null(self, client, transport): + """A None scope list must be dropped, as msrest dropped it. + + Passing None for a required argument is a caller error, but it has to fail the way it + always failed. The AutoRest client sent {} because msrest omitted None fields; sending + {"scopes": null} instead is a different request for a proxy, gateway or log to observe. + This does not assert an exception: the previous client did not raise here either. + """ + try: + client.get_token(CommunicationUserIdentifier("8:acs:u"), None) + except Exception: # pylint: disable=broad-except + pass + assert _sent_body(transport) == {} + + def test_none_create_scopes_are_omitted_not_sent_as_null(self, client, transport): + try: + client.create_user_and_token(None) + except Exception: # pylint: disable=broad-except + pass + assert _sent_body(transport) == {} + + def test_empty_scope_list_is_still_sent(self, client, transport): + """An empty list is a value, not an absence, and must survive.""" + try: + client.get_token(CommunicationUserIdentifier("8:acs:u"), []) + except Exception: # pylint: disable=broad-except + pass + assert _sent_body(transport) == {"scopes": []} + class TestAccessTokenExpiry: """expires_on must be the untouched service string, not a deserialized datetime.""" From 2e2297348d4fc5e5f7ff8960bd5f61ffd03ee23c Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 21:23:02 -0700 Subject: [PATCH 13/16] ACS Identity: send JSON content type for an empty request body Regression introduced by the previous commit. Omitting scopes when None made create_user_and_token(None) serialize to {}, and the two guards that should have covered that case disagree about what "empty" means: generated create(): content_type or "application/json" if body else None body is the dict {} -> falsy -> content_type dropped -> azure-core falls back to text/plain BodylessCreateContentTypePolicy: if http_request.body: return body is the string "{}" -> truthy -> policy skipped So an empty JSON body fell between them. Before the scopes fix the body was {"createTokenWithScopes": null}, which is non-empty, so this never surfaced. The policy now keys on whether a JSON content type is already set rather than on body presence, which is the condition it actually cares about. Measured against the PUBLISHED 1.5.0 package from PyPI - not repo main, whose 1.6.0 was never released - across seven body and header cases: create_user_and_token(None) published: CT=application/json before: CT=text/plain after: CT=application/json All 7 cases now match published 1.5.0 exactly for body, Content-Type and Accept. Adds a regression test. Reverting the policy condition fails it. 216 offline tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure/communication/identity/_utils.py | 6 +++++- .../tests/test_wire_contract.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py index e0ea4af18d5f..ca784b76e0b6 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py @@ -165,7 +165,11 @@ def on_request(self, request) -> None: return if not http_request.url.split("?")[0].endswith("/identities"): return - if http_request.body: + # The generated operation drops content_type whenever the body is falsy, which covers + # both no body at all and an empty JSON object. Both must be repaired, so the test is + # "did the operation fail to set a JSON content type", not "is there a body" -- an + # empty dict serializes to the string "{}", which is truthy and would be missed. + if http_request.headers.get("Content-Type", "").startswith("application/json"): return override = request.context.options.pop(self.CONTEXT_KEY, None) http_request.headers["Content-Type"] = override or "application/json" diff --git a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py index 5d28edaad411..355eb8ecd1f5 100644 --- a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py +++ b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py @@ -171,6 +171,20 @@ def test_none_create_scopes_are_omitted_not_sent_as_null(self, client, transport pass assert _sent_body(transport) == {} + def test_empty_body_still_sends_json_content_type(self, client, transport): + """An empty JSON body must still be typed as JSON. + + The generated operation drops content_type whenever the body is falsy, and an empty + dict is falsy -- so without the policy azure-core falls back to text/plain. The + serialized body is the string "{}", which is truthy, so a body-presence check misses + this case entirely. + """ + try: + client.create_user_and_token(None) + except Exception: # pylint: disable=broad-except + pass + assert transport.requests[-1].headers["Content-Type"] == "application/json" + def test_empty_scope_list_is_still_sent(self, client, transport): """An empty list is a value, not an absence, and must survive.""" try: From 333d8d2e0ede03d4862475cb2cfd9b73001e0674 Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 23:04:46 -0700 Subject: [PATCH 14/16] ACS Identity: pin error-status handling for non-envelope bodies Gateways, front doors, WAFs and throttling pages return HTML or non-envelope JSON on failure, so this path is reached through infrastructure rather than caller error. Customer code branching on e.status_code - retrying on 429, for example - depends on the status surviving a body the deserializer cannot parse. Python is already correct. The generated code raises HttpResponseError(response=response, model=error) taking the status from the response rather than from the parsed model, and _failsafe_deserialize swallows the parse failure and returns None. The equivalent JavaScript client raises a bare TypeError with no status in the same cases, which is the existence proof that this can regress. These tests pin the black-box guarantee rather than the mechanism: they assert the exception type and status code only, not the internals of _failsafe_deserialize. Measured against the shipped package at 7b1196e742 - identical in all five cases, and exception types match across nine statuses including the specialised subclasses (ClientAuthenticationError for 401, ResourceNotFoundError for 404, ResourceExistsError for 409). Mutation: making _failsafe_deserialize propagate instead of swallowing fails three of the four malformed-body cases. 221 offline tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/test_wire_contract.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py index 355eb8ecd1f5..d07284cebdf0 100644 --- a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py +++ b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py @@ -31,6 +31,7 @@ from datetime import timedelta import pytest +from azure.core.exceptions import HttpResponseError from azure.core.pipeline.transport import HttpTransport from azure.communication.identity import ( @@ -77,6 +78,63 @@ def raise_for_status(self): pass +class _RawResponse: + """Returns a body verbatim, so non-JSON and malformed payloads can be exercised.""" + + def __init__(self, request, status_code, raw_body, content_type="application/json"): + self.request = request + self.status_code = status_code + self._body = raw_body + self.headers = {"content-type": content_type} + self.reason = "Error" + self.content_type = content_type + self.is_closed = True + self.is_stream_consumed = True + + @property + def content(self): + return self._body + + def text(self, encoding=None): + return self._body.decode("utf-8", "replace") + + def json(self): + return json.loads(self._body) + + def read(self): + return self._body + + def close(self): + pass + + def raise_for_status(self): + pass + + +class _ErrorTransport(HttpTransport): + """Replies to every request with one canned error response.""" + + def __init__(self, status_code, raw_body, content_type="application/json"): + self.status_code = status_code + self.raw_body = raw_body + self.content_type = content_type + + def send(self, request, **kwargs): + return _RawResponse(request, self.status_code, self.raw_body, self.content_type) + + def open(self): + pass + + def close(self): + pass + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + class _CapturingTransport(HttpTransport): """Records each outgoing request and replies with a canned response.""" @@ -259,6 +317,50 @@ def test_caller_supplied_content_type_is_not_overridden(self, client, transport) assert transport.requests[-1].headers["Content-Type"] == "application/custom" +class TestErrorResponses: + """A non-envelope error body must still surface the real HTTP status. + + Gateways, front doors, WAFs and throttling pages return HTML or non-envelope JSON on + failure, so this is triggered by infrastructure rather than by caller error. Customer + code branching on ``e.status_code`` (for example retrying on 429) depends on the status + surviving a body the deserializer cannot parse. + + The equivalent JavaScript client raises a bare ``TypeError`` with no status in these + cases. Python is correct because the generated code raises + ``HttpResponseError(response=response, model=error)`` -- taking the status from the + response rather than the parsed model -- and ``_failsafe_deserialize`` swallows the parse + failure. These tests pin that black-box guarantee so a future emitter change cannot + regress it silently. + + Assertions are deliberately limited to the exception type and status code. The internals + of ``_failsafe_deserialize`` are not asserted: they are the mechanism, not the contract. + """ + + @pytest.mark.parametrize( + "status_code,raw_body,content_type", + [ + pytest.param(502, b"Bad Gateway", "text/html", id="html_body"), + pytest.param(500, b'{"message":"oops"}', "application/json", id="non_envelope_json"), + pytest.param(503, b"", "application/json", id="empty_body"), + pytest.param(429, b"Too Many Requests", "text/html", id="throttling_html"), + ], + ) + def test_malformed_error_body_still_reports_status(self, status_code, raw_body, content_type): + client = CommunicationIdentityClient( + FAKE_ENDPOINT, FAKE_KEY, transport=_ErrorTransport(status_code, raw_body, content_type) + ) + with pytest.raises(HttpResponseError) as caught: + client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.CHAT]) + assert caught.value.status_code == status_code + + def test_well_formed_error_envelope_still_reports_status(self): + body = b'{"error":{"code":"Unauthorized","message":"denied"}}' + client = CommunicationIdentityClient(FAKE_ENDPOINT, FAKE_KEY, transport=_ErrorTransport(401, body)) + with pytest.raises(HttpResponseError) as caught: + client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.CHAT]) + assert caught.value.status_code == 401 + + class TestApiVersion: """Every request must carry the api-version the package targets. From 35be82c2c61723b28f0e65a2df3c767aaaeda44a Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 23:53:13 -0700 Subject: [PATCH 15/16] Revert full Communication Identity migration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure-communication-identity/CHANGELOG.md | 12 +- .../azure-communication-identity/MANIFEST.in | 6 +- .../_metadata.json | 11 - .../azure-communication-identity/api.md | 2 - .../api.metadata.yml | 4 - .../apiview-properties.json | 37 - .../azure/__init__.py | 2 +- .../azure/communication/__init__.py | 2 +- .../communication/identity/_api_versions.py | 4 +- .../_communication_identity_client.py | 81 +- .../identity/_generated/__init__.py | 19 +- .../identity/_generated/_client.py | 81 +- .../identity/_generated/_configuration.py | 28 +- .../identity/_generated/_patch.py | 38 +- .../serialization.py => _serialization.py} | 870 ++++---- .../identity/_generated/_utils/__init__.py | 6 - .../identity/_generated/_utils/model_base.py | 1787 ----------------- .../identity/_generated/_version.py | 9 - .../identity/_generated/aio/__init__.py | 16 +- .../identity/_generated/aio/_client.py | 83 +- .../identity/_generated/aio/_configuration.py | 28 +- .../identity/_generated/aio/_patch.py | 38 +- .../_generated/aio/operations/__init__.py | 20 +- .../_generated/aio/operations/_operations.py | 940 ++------- .../_generated/aio/operations/_patch.py | 12 +- .../identity/_generated/models/__init__.py | 43 +- .../identity/_generated/models/_enums.py | 13 +- .../identity/_generated/models/_models.py | 476 ++--- .../identity/_generated/models/_patch.py | 12 +- .../_generated/operations/__init__.py | 20 +- .../_generated/operations/_operations.py | 1053 ++-------- .../identity/_generated/operations/_patch.py | 12 +- .../identity/_generated/py.typed | 1 + .../identity/_generated/types.py | 93 - .../azure/communication/identity/_utils.py | 169 +- .../azure/communication/identity/_version.py | 2 +- .../_communication_identity_client_async.py | 62 +- .../azure/communication/identity/py.typed | 1 - .../pyproject.toml | 80 +- .../samples/identity_samples.py | 3 +- .../samples/identity_samples_async.py | 3 +- .../sdk_packaging.toml | 7 + .../_shared/communication_service_preparer.py | 1 - .../tests/conftest.py | 29 - .../tests/test_entra_token_guard_policy.py | 13 +- .../test_entra_token_guard_policy_async.py | 4 +- .../tests/test_identifier_raw_id.py | 128 +- .../tests/test_token_exchange.py | 7 +- .../tests/test_token_exchange_async.py | 7 +- .../tests/test_token_utils.py | 12 +- .../tests/test_user_credential.py | 27 +- .../tests/test_user_credential_async.py | 28 +- .../tests/test_utils.py | 12 +- .../tests/test_wire_contract.py | 388 ---- .../tsp-location.yaml | 4 - 55 files changed, 1309 insertions(+), 5537 deletions(-) delete mode 100644 sdk/communication/azure-communication-identity/_metadata.json delete mode 100644 sdk/communication/azure-communication-identity/api.md delete mode 100644 sdk/communication/azure-communication-identity/api.metadata.yml delete mode 100644 sdk/communication/azure-communication-identity/apiview-properties.json rename sdk/communication/azure-communication-identity/azure/communication/identity/_generated/{_utils/serialization.py => _serialization.py} (72%) delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py create mode 100644 sdk/communication/azure-communication-identity/sdk_packaging.toml delete mode 100644 sdk/communication/azure-communication-identity/tests/test_wire_contract.py delete mode 100644 sdk/communication/azure-communication-identity/tsp-location.yaml diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index e5b3d02f1713..26a278bcd6a3 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -1,25 +1,15 @@ # Release History -## 2.0.0 (Unreleased) +## 1.6.0 (Unreleased) ### Features Added -- Added support for service API version `2026-09-23`, which is now the default. This version is - functionally equivalent to `2025-06-30`. - Added support for a new communication identifier `TeamsExtensionUserIdentifier`. - New identifier maps rawIds with format `8:acs:{resourceId}_{tenantId}_{userId}`. - With this version, rawId starting with `8:acs` may be either `CommunicationUserIdentifier` or new `TeamsExtensionUserIdentifier`. - Added `IsAnonymous` and `AssertedId` properties to the communication identifier `PhoneNumberIdentifier`. - `IsAnonymous` is used for anonymous numbers with rawId equals to `4:anonymous`. - `AssertedId` is used when the same number is used several times in the same call. It contains value after the last underscore `_` character in the phone number. It is null otherwise. - -### Other Changes - -- The service layer is now generated from TypeSpec instead of Swagger/AutoRest. There are no - changes to the public API surface: method names, parameters, and return types are unchanged, - and `AccessToken.expires_on` continues to carry the expiry string exactly as returned by the - service. - ## 1.5.1 (Unreleased) ### Features Added diff --git a/sdk/communication/azure-communication-identity/MANIFEST.in b/sdk/communication/azure-communication-identity/MANIFEST.in index 29aca041172f..7595ab04b9be 100644 --- a/sdk/communication/azure-communication-identity/MANIFEST.in +++ b/sdk/communication/azure-communication-identity/MANIFEST.in @@ -1,7 +1,7 @@ include *.md +include azure/__init__.py +include azure/communication/__init__.py include LICENSE -include azure/communication/identity/py.typed recursive-include tests *.py recursive-include samples *.py *.md -include azure/__init__.py -include azure/communication/__init__.py +include azure/communication/identity/py.typed diff --git a/sdk/communication/azure-communication-identity/_metadata.json b/sdk/communication/azure-communication-identity/_metadata.json deleted file mode 100644 index 08c05f1241b7..000000000000 --- a/sdk/communication/azure-communication-identity/_metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "apiVersion": "2026-09-23", - "apiVersions": { - "Azure.Communication.Identity": "2026-09-23" - }, - "commit": "1893171028aed5a757fc78c2d4aa439c14df4afc", - "repository_url": "https://github.com/Azure/azure-rest-api-specs", - "typespec_src": "specification/communication/data-plane/Identity", - "emitterVersion": "0.63.6", - "httpClientPythonVersion": "^0.37.1" -} \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/api.md b/sdk/communication/azure-communication-identity/api.md deleted file mode 100644 index a75bf12c51ad..000000000000 --- a/sdk/communication/azure-communication-identity/api.md +++ /dev/null @@ -1,2 +0,0 @@ -```py -``` \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/api.metadata.yml b/sdk/communication/azure-communication-identity/api.metadata.yml deleted file mode 100644 index 807cecc73bb7..000000000000 --- a/sdk/communication/azure-communication-identity/api.metadata.yml +++ /dev/null @@ -1,4 +0,0 @@ -apiMdSha256: 4f975ce96bb34064e16820c7565f93dd884f6d02f76ad1b913a9ed1a8eff2e83 -packageVersion: 1.6.0 -parserVersion: 0.3.31 -pythonVersion: 3.13.15 diff --git a/sdk/communication/azure-communication-identity/apiview-properties.json b/sdk/communication/azure-communication-identity/apiview-properties.json deleted file mode 100644 index d60c90497e23..000000000000 --- a/sdk/communication/azure-communication-identity/apiview-properties.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "CrossLanguagePackageId": "Azure.Communication.Identity", - "CrossLanguageDefinitionId": { - "azure.communication.identity.models.CommunicationError": "Azure.Communication.Identity.CommunicationError", - "azure.communication.identity.models.CommunicationErrorResponse": "Azure.Communication.Identity.CommunicationErrorResponse", - "azure.communication.identity.models.CommunicationIdentity": "Azure.Communication.Identity.CommunicationIdentity", - "azure.communication.identity.models.CommunicationIdentityAccessToken": "Azure.Communication.Identity.CommunicationIdentityAccessToken", - "azure.communication.identity.models.CommunicationIdentityAccessTokenRequest": "Azure.Communication.Identity.CommunicationIdentityAccessTokenRequest", - "azure.communication.identity.models.CommunicationIdentityAccessTokenResult": "Azure.Communication.Identity.CommunicationIdentityAccessTokenResult", - "azure.communication.identity.models.CommunicationIdentityCreateRequest": "Azure.Communication.Identity.CommunicationIdentityCreateRequest", - "azure.communication.identity.models.TeamsExtensionAssignmentCreateOrUpdateRequest": "Azure.Communication.Identity.TeamsExtensionAssignmentCreateOrUpdateRequest", - "azure.communication.identity.models.TeamsExtensionAssignmentResponse": "Azure.Communication.Identity.TeamsExtensionAssignmentResponse", - "azure.communication.identity.models.TeamsExtensionExchangeTokenRequest": "Azure.Communication.Identity.TeamsExtensionExchangeTokenRequest", - "azure.communication.identity.models.TeamsUserExchangeTokenRequest": "Azure.Communication.Identity.TeamsUserExchangeTokenRequest", - "azure.communication.identity.models.CommunicationTokenScope": "Azure.Communication.Identity.CommunicationIdentityTokenScope", - "azure.communication.identity.models.TeamsExtensionPrincipalType": "Azure.Communication.Identity.TeamsExtensionPrincipalType", - "azure.communication.identity.operations.IdentityOperationsOperations.create": "Azure.Communication.Identity.IdentityOperations.create", - "azure.communication.identity.aio.operations.IdentityOperationsOperations.create": "Azure.Communication.Identity.IdentityOperations.create", - "azure.communication.identity.operations.IdentityOperationsOperations.delete": "Azure.Communication.Identity.IdentityOperations.delete", - "azure.communication.identity.aio.operations.IdentityOperationsOperations.delete": "Azure.Communication.Identity.IdentityOperations.delete", - "azure.communication.identity.operations.IdentityOperationsOperations.revoke_access_tokens": "Azure.Communication.Identity.IdentityOperations.revokeAccessTokens", - "azure.communication.identity.aio.operations.IdentityOperationsOperations.revoke_access_tokens": "Azure.Communication.Identity.IdentityOperations.revokeAccessTokens", - "azure.communication.identity.operations.IdentityOperationsOperations.issue_access_token": "Azure.Communication.Identity.IdentityOperations.issueAccessToken", - "azure.communication.identity.aio.operations.IdentityOperationsOperations.issue_access_token": "Azure.Communication.Identity.IdentityOperations.issueAccessToken", - "azure.communication.identity.operations.TeamsUserOperationsOperations.exchange_teams_user_access_token": "Azure.Communication.Identity.TeamsUserOperations.exchangeTeamsUserAccessToken", - "azure.communication.identity.aio.operations.TeamsUserOperationsOperations.exchange_teams_user_access_token": "Azure.Communication.Identity.TeamsUserOperations.exchangeTeamsUserAccessToken", - "azure.communication.identity.operations.TeamsExtensionOperationsOperations.exchange_token": "Azure.Communication.Identity.TeamsExtensionOperations.exchangeToken", - "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.exchange_token": "Azure.Communication.Identity.TeamsExtensionOperations.exchangeToken", - "azure.communication.identity.operations.TeamsExtensionOperationsOperations.get_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.getAssignment", - "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.get_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.getAssignment", - "azure.communication.identity.operations.TeamsExtensionOperationsOperations.upsert_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.upsertAssignment", - "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.upsert_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.upsertAssignment", - "azure.communication.identity.operations.TeamsExtensionOperationsOperations.delete_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.deleteAssignment", - "azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations.delete_assignment": "Azure.Communication.Identity.TeamsExtensionOperations.deleteAssignment" - }, - "CrossLanguageVersion": "6be142630ab4" -} \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/azure/__init__.py b/sdk/communication/azure-communication-identity/azure/__init__.py index d55ccad1f573..8db66d3d0f0f 100644 --- a/sdk/communication/azure-communication-identity/azure/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/__init__.py @@ -1 +1 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sdk/communication/azure-communication-identity/azure/communication/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/__init__.py index d55ccad1f573..8db66d3d0f0f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/__init__.py @@ -1 +1 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py index 3a5a5ac8f13b..5e6ef85d3a83 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py @@ -12,8 +12,6 @@ class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): V2022_06_01 = "2022-06-01" V2022_10_01 = "2022-10-01" V2023_10_01 = "2023-10-01" - V2025_06_30 = "2025-06-30" - V2026_09_23 = "2026-09-23" -DEFAULT_VERSION = ApiVersion.V2026_09_23 +DEFAULT_VERSION = ApiVersion.V2023_10_01 diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py index 2cfc1a97648c..ea003e7c564e 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -9,21 +9,14 @@ from azure.core.tracing.decorator import distributed_trace from azure.core.credentials import AccessToken from ._generated._client import ( - IdentityClient as CommunicationIdentityClientGen, + CommunicationIdentityClient as CommunicationIdentityClientGen, ) from ._shared.auth_policy_utils import get_authentication_policy from ._shared.utils import parse_connection_str from ._shared.models import CommunicationUserIdentifier from ._version import SDK_MONIKER from ._api_versions import DEFAULT_VERSION -from ._utils import ( - _ACCEPT_JSON, - BodylessCreateContentTypePolicy, - build_token_request_body, - extract_create_content_type, - merge_headers, - to_access_token, -) +from ._utils import convert_timedelta_to_mins from ._generated.models import CommunicationTokenScope if TYPE_CHECKING: @@ -63,19 +56,20 @@ def __init__( raise ValueError("You need to provide account shared key to authenticate.") self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) - per_call_policies = list(kwargs.pop("per_call_policies", None) or []) - per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential), sdk_moniker=SDK_MONIKER, - per_call_policies=per_call_policies, **kwargs ) @classmethod - def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "CommunicationIdentityClient": + def from_connection_string( + cls, + conn_str: str, + **kwargs: Any + ) -> "CommunicationIdentityClient": """Create CommunicationIdentityClient from a Connection String. :param str conn_str: A connection string to an Azure Communication Service resource. @@ -95,17 +89,14 @@ def create_user(self, **kwargs) -> CommunicationUserIdentifier: :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = self._identity_service_client.identity_operations.create( - **extract_create_content_type(kwargs) - ) + identity_access_token = self._identity_service_client.communication_identity.create(**kwargs) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace def create_user_and_token( self, - scopes: List[Union[str, CommunicationTokenScope]], - *, + scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> Tuple[CommunicationUserIdentifier, AccessToken]: @@ -120,20 +111,30 @@ def create_user_and_token( :rtype: tuple of (~azure.communication.identity.CommunicationUserIdentifier, ~azure.core.credentials.AccessToken) """ - request_body = build_token_request_body("createTokenWithScopes", scopes, token_expires_in) - identity_access_token = self._identity_service_client.identity_operations.create( + request_body = { + "createTokenWithScopes": scopes, + "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), + } + identity_access_token = self._identity_service_client.communication_identity.create( body=request_body, **kwargs # type: ignore ) user_identifier = CommunicationUserIdentifier( identity_access_token.identity.id, raw_id=identity_access_token.identity.id ) - access_token = to_access_token(identity_access_token["accessToken"]) + access_token = AccessToken( + identity_access_token.access_token.token, + identity_access_token.access_token.expires_on, + ) return user_identifier, access_token @distributed_trace - def delete_user(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: + def delete_user( + self, + user: CommunicationUserIdentifier, + **kwargs: Any + ) -> None: """Triggers revocation event for user and deletes all its data. :param user: Azure Communication User to delete @@ -141,16 +142,13 @@ def delete_user(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: :return: None :rtype: None """ - self._identity_service_client.identity_operations.delete( - user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) - ) + self._identity_service_client.communication_identity.delete(user.properties["id"], **kwargs) @distributed_trace def get_token( self, user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], - *, + scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs: Any ) -> AccessToken: @@ -166,16 +164,23 @@ def get_token( :return: AccessToken :rtype: ~azure.core.credentials.AccessToken """ - request_body = build_token_request_body("scopes", scopes, token_expires_in) + request_body = { + "scopes": scopes, + "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), + } - access_token = self._identity_service_client.identity_operations.issue_access_token( + access_token = self._identity_service_client.communication_identity.issue_access_token( user.properties["id"], body=request_body, **kwargs # type: ignore ) - return to_access_token(access_token) + return AccessToken(access_token.token, access_token.expires_on) @distributed_trace - def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs: Any) -> None: + def revoke_tokens( + self, + user: CommunicationUserIdentifier, + **kwargs: Any + ) -> None: """Schedule revocation of all tokens of an identity. :param user: Azure Communication User. @@ -183,13 +188,17 @@ def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs: Any) -> Non :return: None :rtype: None """ - return self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore + return self._identity_service_client.communication_identity.revoke_access_tokens( + user.properties["id"] if user else None, **kwargs # type: ignore ) @distributed_trace def get_token_for_teams_user( - self, aad_token: str, client_id: str, user_object_id: str, **kwargs: Any + self, + aad_token: str, + client_id: str, + user_object_id: str, + **kwargs: Any ) -> AccessToken: """Exchanges an Azure AD access token of a Teams User for a new Communication Identity access token. @@ -211,8 +220,8 @@ def get_token_for_teams_user( "userId": user_object_id, } - access_token = self._identity_service_client.teams_user_operations.exchange_teams_user_access_token( + access_token = self._identity_service_client.communication_identity.exchange_teams_user_access_token( body=request_body, **kwargs # type: ignore ) - return to_access_token(access_token) + return AccessToken(access_token.token, access_token.expires_on) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py index d4c9c7017d4c..7d62ac6effd1 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/__init__.py @@ -2,31 +2,22 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: disable=wrong-import-position -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from ._patch import * # pylint: disable=unused-wildcard-import - -from ._client import IdentityClient # type: ignore -from ._version import VERSION - -__version__ = VERSION +from ._client import CommunicationIdentityClient try: from ._patch import __all__ as _patch_all - from ._patch import * + from ._patch import * # pylint: disable=unused-wildcard-import except ImportError: _patch_all = [] from ._patch import patch_sdk as _patch_sdk __all__ = [ - "IdentityClient", + "CommunicationIdentityClient", ] -__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore +__all__.extend([p for p in _patch_all if p not in __all__]) _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py index dd116886cecc..45e7da9919ec 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_client.py @@ -2,47 +2,33 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from copy import deepcopy -import sys from typing import Any from azure.core import PipelineClient -from azure.core.pipeline import policies from azure.core.rest import HttpRequest, HttpResponse -from ._configuration import IdentityClientConfiguration -from ._utils.serialization import Deserializer, Serializer -from .operations import IdentityOperationsOperations, TeamsExtensionOperationsOperations, TeamsUserOperationsOperations +from . import models as _models +from ._configuration import CommunicationIdentityClientConfiguration +from ._serialization import Deserializer, Serializer +from .operations import CommunicationIdentityOperations -if sys.version_info >= (3, 11): - from typing import Self -else: - from typing_extensions import Self # type: ignore - -class IdentityClient: # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationIdentityClient: # pylint: disable=client-accepts-api-version-keyword """Azure Communication Identity Service. - :ivar identity_operations: IdentityOperationsOperations operations - :vartype identity_operations: - azure.communication.identity.operations.IdentityOperationsOperations - :ivar teams_user_operations: TeamsUserOperationsOperations operations - :vartype teams_user_operations: - azure.communication.identity.operations.TeamsUserOperationsOperations - :ivar teams_extension_operations: TeamsExtensionOperationsOperations operations - :vartype teams_extension_operations: - azure.communication.identity.operations.TeamsExtensionOperationsOperations + :ivar communication_identity: CommunicationIdentityOperations operations + :vartype communication_identity: + azure.communication.identity.operations.CommunicationIdentityOperations :param endpoint: The communication resource, for example - `https://my-resource.communication.azure.com `_. - Required. + https://my-resource.communication.azure.com. Required. :type endpoint: str - :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this + default value may result in unsupported behavior. :paramtype api_version: str """ @@ -50,41 +36,18 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self, endpoint: str, **kwargs: Any ) -> None: _endpoint = "{endpoint}" - self._config = IdentityClientConfiguration(endpoint=endpoint, **kwargs) - - _policies = kwargs.pop("policies", None) - if _policies is None: - _policies = [ - policies.RequestIdPolicy(**kwargs), - self._config.headers_policy, - self._config.user_agent_policy, - self._config.proxy_policy, - policies.ContentDecodePolicy(**kwargs), - self._config.redirect_policy, - self._config.retry_policy, - self._config.authentication_policy, - self._config.custom_hook_policy, - self._config.logging_policy, - policies.DistributedTracingPolicy(**kwargs), - policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None, - self._config.http_logging_policy, - ] - self._client: PipelineClient = PipelineClient(base_url=_endpoint, policies=_policies, **kwargs) - - self._serialize = Serializer() - self._deserialize = Deserializer() + self._config = CommunicationIdentityClientConfiguration(endpoint=endpoint, **kwargs) + self._client: PipelineClient = PipelineClient(base_url=_endpoint, config=self._config, **kwargs) + + client_models = {k: v for k, v in _models.__dict__.items() if isinstance(v, type)} + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) self._serialize.client_side_validation = False - self.identity_operations = IdentityOperationsOperations( - self._client, self._config, self._serialize, self._deserialize - ) - self.teams_user_operations = TeamsUserOperationsOperations( - self._client, self._config, self._serialize, self._deserialize - ) - self.teams_extension_operations = TeamsExtensionOperationsOperations( + self.communication_identity = CommunicationIdentityOperations( self._client, self._config, self._serialize, self._deserialize ) - def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: + def send_request(self, request: HttpRequest, **kwargs: Any) -> HttpResponse: """Runs the network request through the client's chained policies. >>> from azure.core.rest import HttpRequest @@ -108,12 +71,12 @@ def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: } request_copy.url = self._client.format_url(request_copy.url, **path_format_arguments) - return self._client.send_request(request_copy, stream=stream, **kwargs) # type: ignore + return self._client.send_request(request_copy, **kwargs) def close(self) -> None: self._client.close() - def __enter__(self) -> Self: + def __enter__(self) -> "CommunicationIdentityClient": self._client.__enter__() return self diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py index 8e224fc5e630..3ba79d8d9d97 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_configuration.py @@ -1,45 +1,45 @@ -# pylint: disable=line-too-long,useless-suppression # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from typing import Any +from azure.core.configuration import Configuration from azure.core.pipeline import policies -from ._version import VERSION +VERSION = "unknown" -class IdentityClientConfiguration: # pylint: disable=too-many-instance-attributes,docstring-keyword-should-match-keyword-only - """Configuration for IdentityClient. +class CommunicationIdentityClientConfiguration( # pylint: disable=too-many-instance-attributes,name-too-long + Configuration +): + """Configuration for CommunicationIdentityClient. Note that all parameters used to create this instance are saved as instance attributes. :param endpoint: The communication resource, for example - `https://my-resource.communication.azure.com `_. - Required. + https://my-resource.communication.azure.com. Required. :type endpoint: str - :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this + default value may result in unsupported behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2026-09-23") + super(CommunicationIdentityClientConfiguration, self).__init__(**kwargs) + api_version: str = kwargs.pop("api_version", "2023-10-01") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") self.endpoint = endpoint self.api_version = api_version - kwargs.setdefault("sdk_moniker", "communication-identity/{}".format(VERSION)) - self.polling_interval = kwargs.get("polling_interval", 30) + kwargs.setdefault("sdk_moniker", "communicationidentityclient/{}".format(VERSION)) self._configure(**kwargs) def _configure(self, **kwargs: Any) -> None: @@ -48,7 +48,7 @@ def _configure(self, **kwargs: Any) -> None: self.proxy_policy = kwargs.get("proxy_policy") or policies.ProxyPolicy(**kwargs) self.logging_policy = kwargs.get("logging_policy") or policies.NetworkTraceLoggingPolicy(**kwargs) self.http_logging_policy = kwargs.get("http_logging_policy") or policies.HttpLoggingPolicy(**kwargs) + self.retry_policy = kwargs.get("retry_policy") or policies.RetryPolicy(**kwargs) self.custom_hook_policy = kwargs.get("custom_hook_policy") or policies.CustomHookPolicy(**kwargs) self.redirect_policy = kwargs.get("redirect_policy") or policies.RedirectPolicy(**kwargs) - self.retry_policy = kwargs.get("retry_policy") or policies.RetryPolicy(**kwargs) self.authentication_policy = kwargs.get("authentication_policy") diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py index ea765788358a..17dbc073e01b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_patch.py @@ -1,20 +1,32 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. +# +# The MIT License (MIT) +# +# 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. +# # -------------------------------------------------------------------------- -"""Customize generated code here. - -Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize -""" - -__all__: list[str] = [] # Add all objects you want publicly available to users at this package level +# This file is used for handwritten extensions to the generated code. Example: +# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/customize_code/how-to-patch-sdk-code.md def patch_sdk(): - """Do not remove from this file. - - `patch_sdk` is a last resort escape hatch that allows you to do customizations - you can't accomplish using the techniques described in - https://aka.ms/azsdk/python/dpcodegen/python/customize - """ + pass diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/serialization.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_serialization.py similarity index 72% rename from sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/serialization.py rename to sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_serialization.py index ae08f9d89f74..c13084631923 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/serialization.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_serialization.py @@ -1,12 +1,30 @@ -# pylint: disable=line-too-long,useless-suppression,too-many-lines -# coding=utf-8 # -------------------------------------------------------------------------- +# # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# +# The MIT License (MIT) +# +# 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. +# # -------------------------------------------------------------------------- +# pylint: skip-file # pyright: reportUnnecessaryTypeIgnoreComment=false from base64 import b64decode, b64encode @@ -21,6 +39,7 @@ import sys import codecs from typing import ( + Dict, Any, cast, Optional, @@ -29,7 +48,11 @@ IO, Mapping, Callable, + TypeVar, MutableMapping, + Type, + List, + Mapping, ) try: @@ -40,21 +63,20 @@ import isodate # type: ignore -from azure.core.exceptions import DeserializationError, SerializationError -from azure.core.serialization import NULL as CoreNull - -if sys.version_info >= (3, 11): - from typing import Self -else: - from typing_extensions import Self +from azure.core.exceptions import ( + DeserializationError, + SerializationError, + raise_with_traceback, +) +from azure.core.serialization import NULL as AzureCoreNull _BOM = codecs.BOM_UTF8.decode(encoding="utf-8") +ModelType = TypeVar("ModelType", bound="Model") JSON = MutableMapping[str, Any] class RawDeserializer: - # Accept "text" because we're open minded people... JSON_REGEXP = re.compile(r"^(application|text)/([a-z+.]+\+)?json$") @@ -72,8 +94,6 @@ def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: :param data: Input, could be bytes or stream (will be decoded with UTF8) or text :type data: str or bytes or IO :param str content_type: The content type. - :return: The deserialized data. - :rtype: object """ if hasattr(data, "read"): # Assume a stream @@ -95,10 +115,9 @@ def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: try: return json.loads(data_as_str) except ValueError as err: - raise DeserializationError("JSON is invalid: {}".format(err), err) from err + raise DeserializationError("JSON is invalid: {}".format(err), err) elif "xml" in (content_type or []): try: - try: if isinstance(data, unicode): # type: ignore # If I'm Python 2.7 and unicode XML will scream if I try a "fromstring" on unicode string @@ -107,7 +126,7 @@ def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: pass return ET.fromstring(data_as_str) # nosec - except ET.ParseError as err: + except ET.ParseError: # It might be because the server has an issue, and returned JSON with # content-type XML.... # So let's try a JSON load, and if it's still broken @@ -126,9 +145,7 @@ def _json_attemp(data): # The function hack is because Py2.7 messes up with exception # context otherwise. _LOGGER.critical("Wasn't XML not JSON, failing") - raise DeserializationError("XML is invalid") from err - elif content_type.startswith("text/"): - return data_as_str + raise_with_traceback(DeserializationError, "XML is invalid") raise DeserializationError("Cannot deserialize content-type: {}".format(content_type)) @classmethod @@ -138,11 +155,6 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], Use bytes and headers to NOT use any requests/aiohttp or whatever specific implementation. Headers will tested for "content-type" - - :param bytes body_bytes: The body of the response. - :param dict headers: The headers of the response. - :returns: The deserialized data. - :rtype: object """ # Try to use content-type from headers if available content_type = None @@ -160,6 +172,13 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], return None +try: + basestring # type: ignore + unicode_str = unicode # type: ignore +except NameError: + basestring = str + unicode_str = str + _LOGGER = logging.getLogger(__name__) try: @@ -167,31 +186,80 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], except NameError: _long_type = int -TZ_UTC = datetime.timezone.utc + +class UTC(datetime.tzinfo): + """Time Zone info for handling UTC""" + + def utcoffset(self, dt): + """UTF offset for UTC is 0.""" + return datetime.timedelta(0) + + def tzname(self, dt): + """Timestamp representation.""" + return "Z" + + def dst(self, dt): + """No daylight saving for UTC.""" + return datetime.timedelta(hours=1) + + +try: + from datetime import timezone as _FixedOffset # type: ignore +except ImportError: # Python 2.7 + + class _FixedOffset(datetime.tzinfo): # type: ignore + """Fixed offset in minutes east from UTC. + Copy/pasted from Python doc + :param datetime.timedelta offset: offset in timedelta format + """ + + def __init__(self, offset): + self.__offset = offset + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return str(self.__offset.total_seconds() / 3600) + + def __repr__(self): + return "".format(self.tzname(None)) + + def dst(self, dt): + return datetime.timedelta(0) + + def __getinitargs__(self): + return (self.__offset,) + + +try: + from datetime import timezone + + TZ_UTC = timezone.utc +except ImportError: + TZ_UTC = UTC() # type: ignore _FLATTEN = re.compile(r"(? None: - self.additional_properties: Optional[dict[str, Any]] = {} - for k in kwargs: # pylint: disable=consider-using-dict-items + self.additional_properties: Dict[str, Any] = {} + for k in kwargs: if k not in self._attribute_map: - _LOGGER.warning("%s is not a known attribute of class %s and will be ignored", k, self.__class__) + _LOGGER.warning( + "%s is not a known attribute of class %s and will be ignored", + k, + self.__class__, + ) elif k in self._validation and self._validation[k].get("readonly", False): - _LOGGER.warning("Readonly attribute %s will be ignored in class %s", k, self.__class__) + _LOGGER.warning( + "Readonly attribute %s will be ignored in class %s", + k, + self.__class__, + ) else: setattr(self, k, kwargs[k]) def __eq__(self, other: Any) -> bool: - """Compare objects by comparing all attributes. - - :param object other: The object to compare - :returns: True if objects are equal - :rtype: bool - """ + """Compare objects by comparing all attributes.""" if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ return False def __ne__(self, other: Any) -> bool: - """Compare objects by comparing all attributes. - - :param object other: The object to compare - :returns: True if objects are not equal - :rtype: bool - """ + """Compare objects by comparing all attributes.""" return not self.__eq__(other) def __str__(self) -> str: @@ -282,20 +341,20 @@ def is_xml_model(cls) -> bool: @classmethod def _create_xml_node(cls): - """Create XML node. - - :returns: The XML node - :rtype: xml.etree.ElementTree.Element - """ + """Create XML node.""" try: xml_map = cls._xml_map # type: ignore except AttributeError: xml_map = {} - return _create_xml_node(xml_map.get("name", cls.__name__), xml_map.get("prefix", None), xml_map.get("ns", None)) + return _create_xml_node( + xml_map.get("name", cls.__name__), + xml_map.get("prefix", None), + xml_map.get("ns", None), + ) def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: - """Return the JSON that would be sent to server from this model. + """Return the JSON that would be sent to azure from this model. This is an alias to `as_dict(full_restapi_key_transformer, keep_readonly=False)`. @@ -306,14 +365,12 @@ def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: :rtype: dict """ serializer = Serializer(self._infer_class_models()) - return serializer._serialize( # type: ignore # pylint: disable=protected-access - self, keep_readonly=keep_readonly, **kwargs - ) + return serializer._serialize(self, keep_readonly=keep_readonly, **kwargs) def as_dict( self, keep_readonly: bool = True, - key_transformer: Callable[[str, dict[str, Any], Any], Any] = attribute_transformer, + key_transformer: Callable[[str, Dict[str, Any], Any], Any] = attribute_transformer, **kwargs: Any ) -> JSON: """Return a dict that can be serialized using json.dump. @@ -342,15 +399,12 @@ def my_key_transformer(key, attr_desc, value): If you want XML serialization, you can pass the kwargs is_xml=True. - :param bool keep_readonly: If you want to serialize the readonly attributes :param function key_transformer: A key transformer function. :returns: A dict JSON compatible object :rtype: dict """ serializer = Serializer(self._infer_class_models()) - return serializer._serialize( # type: ignore # pylint: disable=protected-access - self, key_transformer=key_transformer, keep_readonly=keep_readonly, **kwargs - ) + return serializer._serialize(self, key_transformer=key_transformer, keep_readonly=keep_readonly, **kwargs) @classmethod def _infer_class_models(cls): @@ -360,31 +414,30 @@ def _infer_class_models(cls): client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} if cls.__name__ not in client_models: raise ValueError("Not Autorest generated code") - except Exception: # pylint: disable=broad-exception-caught + except Exception: # Assume it's not Autorest generated (tests?). Add ourselves as dependencies. client_models = {cls.__name__: cls} return client_models @classmethod - def deserialize(cls, data: Any, content_type: Optional[str] = None) -> Self: + def deserialize(cls: Type[ModelType], data: Any, content_type: Optional[str] = None) -> ModelType: """Parse a str using the RestAPI syntax and return a model. :param str data: A str using RestAPI structure. JSON by default. :param str content_type: JSON by default, set application/xml if XML. :returns: An instance of this model - :raises DeserializationError: if something went wrong - :rtype: Self + :raises: DeserializationError if something went wrong """ deserializer = Deserializer(cls._infer_class_models()) - return deserializer(cls.__name__, data, content_type=content_type) # type: ignore + return deserializer(cls.__name__, data, content_type=content_type) @classmethod def from_dict( - cls, + cls: Type[ModelType], data: Any, - key_extractors: Optional[Callable[[str, dict[str, Any], Any], Any]] = None, + key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, content_type: Optional[str] = None, - ) -> Self: + ) -> ModelType: """Parse a dict using given key extractor return a model. By default consider key @@ -392,11 +445,9 @@ def from_dict( and last_rest_key_case_insensitive_extractor) :param dict data: A dict using RestAPI structure - :param function key_extractors: A key extractor function. :param str content_type: JSON by default, set application/xml if XML. :returns: An instance of this model - :raises DeserializationError: if something went wrong - :rtype: Self + :raises: DeserializationError if something went wrong """ deserializer = Deserializer(cls._infer_class_models()) deserializer.key_extractors = ( # type: ignore @@ -408,7 +459,7 @@ def from_dict( if key_extractors is None else key_extractors ) - return deserializer(cls.__name__, data, content_type=content_type) # type: ignore + return deserializer(cls.__name__, data, content_type=content_type) @classmethod def _flatten_subtype(cls, key, objects): @@ -416,25 +467,21 @@ def _flatten_subtype(cls, key, objects): return {} result = dict(cls._subtype_map[key]) for valuetype in cls._subtype_map[key].values(): - result |= objects[valuetype]._flatten_subtype(key, objects) # pylint: disable=protected-access + result.update(objects[valuetype]._flatten_subtype(key, objects)) return result @classmethod def _classify(cls, response, objects): """Check the class _subtype_map for any child classes. We want to ignore any inherited _subtype_maps. - - :param dict response: The initial data - :param dict objects: The class objects - :returns: The class to be used - :rtype: class + Remove the polymorphic key from the initial data. """ for subtype_key in cls.__dict__.get("_subtype_map", {}).keys(): subtype_value = None if not isinstance(response, ET.Element): rest_api_response_key = cls._get_rest_key_parts(subtype_key)[-1] - subtype_value = response.get(rest_api_response_key, None) or response.get(subtype_key, None) + subtype_value = response.pop(rest_api_response_key, None) or response.pop(subtype_key, None) else: subtype_value = xml_key_extractor(subtype_key, cls._attribute_map[subtype_key], response) if subtype_value: @@ -453,7 +500,11 @@ def _classify(cls, response, objects): ) break else: - _LOGGER.warning("Discriminator %s is absent or null, use base class %s.", subtype_key, cls.__name__) + _LOGGER.warning( + "Discriminator %s is absent or null, use base class %s.", + subtype_key, + cls.__name__, + ) break return cls @@ -473,18 +524,12 @@ def _decode_attribute_map_key(key): inside the received data. :param str key: A key string from the generated code - :returns: The decoded key - :rtype: str """ return key.replace("\\.", ".") -class Serializer: # pylint: disable=too-many-public-methods - """Request object model serializer. - - :param classes: Mapping of model names to model types, used to resolve models during serialization. - :type classes: typing.Optional[typing.Mapping[str, type]] - """ +class Serializer(object): + """Request object model serializer.""" basic_types = {str: "str", int: "int", bool: "bool", float: "float"} @@ -518,16 +563,12 @@ class Serializer: # pylint: disable=too-many-public-methods "multiple": lambda x, y: x % y != 0, } - def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: + def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): self.serialize_type = { "iso-8601": Serializer.serialize_iso, "rfc-1123": Serializer.serialize_rfc, "unix-time": Serializer.serialize_unix, "duration": Serializer.serialize_duration, - "duration-seconds-int": Serializer.serialize_duration_seconds_int, - "duration-seconds-float": Serializer.serialize_duration_seconds_float, - "duration-milliseconds-int": Serializer.serialize_duration_milliseconds_int, - "duration-milliseconds-float": Serializer.serialize_duration_milliseconds_float, "date": Serializer.serialize_date, "time": Serializer.serialize_time, "decimal": Serializer.serialize_decimal, @@ -538,20 +579,17 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: "[]": self.serialize_iter, "{}": self.serialize_dict, } - self.dependencies: dict[str, type] = dict(classes) if classes else {} + self.dependencies: Dict[str, Type[ModelType]] = dict(classes) if classes else {} self.key_transformer = full_restapi_key_transformer self.client_side_validation = True - def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, too-many-statements, too-many-locals - self, target_obj, data_type=None, **kwargs - ): + def _serialize(self, target_obj, data_type=None, **kwargs): """Serialize data into a string according to type. - :param object target_obj: The data to be serialized. + :param target_obj: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str, dict - :raises SerializationError: if serialization fails. - :returns: The serialized data. + :raises: SerializationError if serialization fails. """ key_transformer = kwargs.get("key_transformer", self.key_transformer) keep_readonly = kwargs.get("keep_readonly", False) @@ -577,22 +615,19 @@ def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, to serialized = {} if is_xml_model_serialization: - serialized = target_obj._create_xml_node() # pylint: disable=protected-access + serialized = target_obj._create_xml_node() try: - attributes = target_obj._attribute_map # pylint: disable=protected-access + attributes = target_obj._attribute_map for attr, attr_desc in attributes.items(): attr_name = attr - if not keep_readonly and target_obj._validation.get( # pylint: disable=protected-access - attr_name, {} - ).get("readonly", False): + if not keep_readonly and target_obj._validation.get(attr_name, {}).get("readonly", False): continue if attr_name == "additional_properties" and attr_desc["key"] == "": if target_obj.additional_properties is not None: - serialized |= target_obj.additional_properties + serialized.update(target_obj.additional_properties) continue try: - orig_attr = getattr(target_obj, attr) if is_xml_model_serialization: pass # Don't provide "transformer" for XML for now. Keep "orig_attr" @@ -620,8 +655,7 @@ def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, to if isinstance(new_attr, list): serialized.extend(new_attr) # type: ignore elif isinstance(new_attr, ET.Element): - # If the down XML has no XML/Name, - # we MUST replace the tag with the local tag. But keeping the namespaces. + # If the down XML has no XML/Name, we MUST replace the tag with the local tag. But keeping the namespaces. if "name" not in getattr(orig_attr, "_xml_map", {}): splitted_tag = new_attr.tag.split("}") if len(splitted_tag) == 2: # Namespace @@ -632,7 +666,7 @@ def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, to else: # That's a basic type # Integrate namespace if necessary local_node = _create_xml_node(xml_name, xml_prefix, xml_ns) - local_node.text = str(new_attr) + local_node.text = unicode_str(new_attr) serialized.append(local_node) # type: ignore else: # JSON for k in reversed(keys): # type: ignore @@ -651,18 +685,18 @@ def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, to except (AttributeError, KeyError, TypeError) as err: msg = "Attribute {} in object {} cannot be serialized.\n{}".format(attr_name, class_name, str(target_obj)) - raise SerializationError(msg) from err - return serialized + raise_with_traceback(SerializationError, msg, err) + else: + return serialized def body(self, data, data_type, **kwargs): """Serialize data intended for a request body. - :param object data: The data to be serialized. + :param data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: dict - :raises SerializationError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized request body + :raises: SerializationError if serialization fails. + :raises: ValueError if data is None """ # Just in case this is a dict @@ -691,22 +725,20 @@ def body(self, data, data_type, **kwargs): attribute_key_case_insensitive_extractor, last_rest_key_case_insensitive_extractor, ] - data = deserializer._deserialize(data_type, data) # pylint: disable=protected-access + data = deserializer._deserialize(data_type, data) except DeserializationError as err: - raise SerializationError("Unable to build a model: " + str(err)) from err + raise_with_traceback(SerializationError, "Unable to build a model: " + str(err), err) return self._serialize(data, data_type, **kwargs) def url(self, name, data, data_type, **kwargs): """Serialize data intended for a URL path. - :param str name: The name of the URL path parameter. - :param object data: The data to be serialized. + :param data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str - :returns: The serialized URL path - :raises TypeError: if serialization fails. - :raises ValueError: if data is None + :raises: TypeError if serialization fails. + :raises: ValueError if data is None """ try: output = self.serialize_data(data, data_type, **kwargs) @@ -715,30 +747,32 @@ def url(self, name, data, data_type, **kwargs): if kwargs.get("skip_quote") is True: output = str(output) + # https://github.com/Azure/autorest.python/issues/2063 output = output.replace("{", quote("{")).replace("}", quote("}")) else: output = quote(str(output), safe="") - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return output + except SerializationError: + raise TypeError("{} must be type {}.".format(name, data_type)) + else: + return output def query(self, name, data, data_type, **kwargs): """Serialize data intended for a URL query. - :param str name: The name of the query parameter. - :param object data: The data to be serialized. + :param data: The data to be serialized. :param str data_type: The type to be serialized from. - :rtype: str, list - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized query parameter + :keyword bool skip_quote: Whether to skip quote the serialized result. + Defaults to False. + :rtype: str + :raises: TypeError if serialization fails. + :raises: ValueError if data is None """ try: # Treat the list aside, since we don't want to encode the div separator if data_type.startswith("["): internal_data_type = data_type[1:-1] do_quote = not kwargs.get("skip_quote", False) - return self.serialize_iter(data, internal_data_type, do_quote=do_quote, **kwargs) + return str(self.serialize_iter(data, internal_data_type, do_quote=do_quote, **kwargs)) # Not a list, regular serialization output = self.serialize_data(data, data_type, **kwargs) @@ -748,20 +782,19 @@ def query(self, name, data, data_type, **kwargs): output = str(output) else: output = quote(str(output), safe="") - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return str(output) + except SerializationError: + raise TypeError("{} must be type {}.".format(name, data_type)) + else: + return str(output) def header(self, name, data, data_type, **kwargs): """Serialize data intended for a request header. - :param str name: The name of the header. - :param object data: The data to be serialized. + :param data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized header + :raises: TypeError if serialization fails. + :raises: ValueError if data is None """ try: if data_type in ["[str]"]: @@ -770,36 +803,37 @@ def header(self, name, data, data_type, **kwargs): output = self.serialize_data(data, data_type, **kwargs) if data_type == "bool": output = json.dumps(output) - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return str(output) + except SerializationError: + raise TypeError("{} must be type {}.".format(name, data_type)) + else: + return str(output) def serialize_data(self, data, data_type, **kwargs): """Serialize generic data according to supplied data type. - :param object data: The data to be serialized. + :param data: The data to be serialized. :param str data_type: The type to be serialized from. - :raises AttributeError: if required data is None. - :raises ValueError: if data is None - :raises SerializationError: if serialization fails. - :returns: The serialized data. - :rtype: str, int, float, bool, dict, list + :param bool required: Whether it's essential that the data not be + empty or None + :raises: AttributeError if required data is None. + :raises: ValueError if data is None + :raises: SerializationError if serialization fails. """ if data is None: raise ValueError("No value for given attribute") try: - if data is CoreNull: + if data is AzureCoreNull: return None if data_type in self.basic_types.values(): return self.serialize_basic(data, data_type, **kwargs) - if data_type in self.serialize_type: + elif data_type in self.serialize_type: return self.serialize_type[data_type](data, **kwargs) # If dependencies is empty, try with current data class # It has to be a subclass of Enum anyway - enum_type = self.dependencies.get(data_type, cast(type, data.__class__)) + enum_type = self.dependencies.get(data_type, data.__class__) if issubclass(enum_type, Enum): return Serializer.serialize_enum(data, enum_obj=enum_type) @@ -809,11 +843,12 @@ def serialize_data(self, data, data_type, **kwargs): except (ValueError, TypeError) as err: msg = "Unable to serialize value: {!r} as type: {!r}." - raise SerializationError(msg.format(data, data_type)) from err - return self._serialize(data, **kwargs) + raise_with_traceback(SerializationError, msg.format(data, data_type), err) + else: + return self._serialize(data, **kwargs) @classmethod - def _get_custom_serializers(cls, data_type, **kwargs): # pylint: disable=inconsistent-return-statements + def _get_custom_serializers(cls, data_type, **kwargs): custom_serializer = kwargs.get("basic_types_serializers", {}).get(data_type) if custom_serializer: return custom_serializer @@ -829,33 +864,23 @@ def serialize_basic(cls, data, data_type, **kwargs): - basic_types_serializers dict[str, callable] : If set, use the callable as serializer - is_xml bool : If set, use xml_basic_types_serializers - :param obj data: Object to be serialized. + :param data: Object to be serialized. :param str data_type: Type of object in the iterable. - :rtype: str, int, float, bool - :return: serialized object - :raises TypeError: raise if data_type is not one of str, int, float, bool. """ custom_serializer = cls._get_custom_serializers(data_type, **kwargs) if custom_serializer: return custom_serializer(data) if data_type == "str": return cls.serialize_unicode(data) - if data_type == "int": - return int(data) - if data_type == "float": - return float(data) - if data_type == "bool": - return bool(data) - raise TypeError("Unknown basic data type: {}".format(data_type)) + return eval(data_type)(data) # nosec @classmethod def serialize_unicode(cls, data): """Special handling for serializing unicode strings in Py2. Encode to UTF-8 if unicode, otherwise handle as a str. - :param str data: Object to be serialized. + :param data: Object to be serialized. :rtype: str - :return: serialized object """ try: # If I received an enum, return its value return data.value @@ -869,7 +894,8 @@ def serialize_unicode(cls, data): return data except NameError: return str(data) - return str(data) + else: + return str(data) def serialize_iter(self, data, iter_type, div=None, **kwargs): """Serialize iterable. @@ -879,13 +905,15 @@ def serialize_iter(self, data, iter_type, div=None, **kwargs): serialization_ctxt['type'] should be same as data_type. - is_xml bool : If set, serialize as XML - :param list data: Object to be serialized. + :param list attr: Object to be serialized. :param str iter_type: Type of object in the iterable. + :param bool required: Whether the objects in the iterable must + not be None or empty. :param str div: If set, this str will be used to combine the elements in the iterable into a combined string. Default is 'None'. + :keyword bool do_quote: Whether to quote the serialized result of each iterable element. Defaults to False. :rtype: list, str - :return: serialized iterable """ if isinstance(data, str): raise SerializationError("Refuse str type as a valid iter type.") @@ -928,7 +956,11 @@ def serialize_iter(self, data, iter_type, div=None, **kwargs): if isinstance(el, ET.Element): el_node = el else: - el_node = _create_xml_node(node_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) + el_node = _create_xml_node( + node_name, + xml_desc.get("prefix", None), + xml_desc.get("ns", None), + ) if el is not None: # Otherwise it writes "None" :-p el_node.text = str(el) final_result.append(el_node) @@ -940,8 +972,9 @@ def serialize_dict(self, attr, dict_type, **kwargs): :param dict attr: Object to be serialized. :param str dict_type: Type of object in the dictionary. + :param bool required: Whether the objects in the dictionary must + not be None or empty. :rtype: dict - :return: serialized dictionary """ serialization_ctxt = kwargs.get("serialization_ctxt", {}) serialized = {} @@ -965,7 +998,7 @@ def serialize_dict(self, attr, dict_type, **kwargs): return serialized - def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements + def serialize_object(self, attr, **kwargs): """Serialize a generic object. This will be handled as a dictionary. If object passed in is not a basic type (str, int, float, dict, list) it will simply be @@ -973,7 +1006,6 @@ def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-s :param dict attr: Object to be serialized. :rtype: dict or str - :return: serialized object """ if attr is None: return None @@ -984,7 +1016,7 @@ def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-s return self.serialize_basic(attr, self.basic_types[obj_type], **kwargs) if obj_type is _long_type: return self.serialize_long(attr) - if obj_type is str: + if obj_type is unicode_str: return self.serialize_unicode(attr) if obj_type is datetime.datetime: return self.serialize_iso(attr) @@ -998,7 +1030,7 @@ def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-s return self.serialize_decimal(attr) # If it's a model or I know this dependency, serialize as a Model - if obj_type in self.dependencies.values() or isinstance(attr, Model): + elif obj_type in self.dependencies.values() or isinstance(attr, Model): return self._serialize(attr) if obj_type == dict: @@ -1029,61 +1061,56 @@ def serialize_enum(attr, enum_obj=None): try: enum_obj(result) # type: ignore return result - except ValueError as exc: + except ValueError: for enum_value in enum_obj: # type: ignore if enum_value.value.lower() == str(attr).lower(): return enum_value.value error = "{!r} is not valid value for enum {!r}" - raise SerializationError(error.format(attr, enum_obj)) from exc + raise SerializationError(error.format(attr, enum_obj)) @staticmethod - def serialize_bytearray(attr, **kwargs): # pylint: disable=unused-argument + def serialize_bytearray(attr, **kwargs): """Serialize bytearray into base-64 string. - :param str attr: Object to be serialized. + :param attr: Object to be serialized. :rtype: str - :return: serialized base64 """ return b64encode(attr).decode() @staticmethod - def serialize_base64(attr, **kwargs): # pylint: disable=unused-argument + def serialize_base64(attr, **kwargs): """Serialize str into base-64 string. - :param str attr: Object to be serialized. + :param attr: Object to be serialized. :rtype: str - :return: serialized base64 """ encoded = b64encode(attr).decode("ascii") return encoded.strip("=").replace("+", "-").replace("/", "_") @staticmethod - def serialize_decimal(attr, **kwargs): # pylint: disable=unused-argument + def serialize_decimal(attr, **kwargs): """Serialize Decimal object to float. - :param decimal attr: Object to be serialized. + :param attr: Object to be serialized. :rtype: float - :return: serialized decimal """ return float(attr) @staticmethod - def serialize_long(attr, **kwargs): # pylint: disable=unused-argument + def serialize_long(attr, **kwargs): """Serialize long (Py2) or int (Py3). - :param int attr: Object to be serialized. + :param attr: Object to be serialized. :rtype: int/long - :return: serialized long """ return _long_type(attr) @staticmethod - def serialize_date(attr, **kwargs): # pylint: disable=unused-argument + def serialize_date(attr, **kwargs): """Serialize Date object into ISO-8601 formatted string. :param Date attr: Object to be serialized. :rtype: str - :return: serialized date """ if isinstance(attr, str): attr = isodate.parse_date(attr) @@ -1091,12 +1118,11 @@ def serialize_date(attr, **kwargs): # pylint: disable=unused-argument return t @staticmethod - def serialize_time(attr, **kwargs): # pylint: disable=unused-argument + def serialize_time(attr, **kwargs): """Serialize Time object into ISO-8601 formatted string. :param datetime.time attr: Object to be serialized. :rtype: str - :return: serialized time """ if isinstance(attr, str): attr = isodate.parse_time(attr) @@ -1106,87 +1132,30 @@ def serialize_time(attr, **kwargs): # pylint: disable=unused-argument return t @staticmethod - def serialize_duration(attr, **kwargs): # pylint: disable=unused-argument + def serialize_duration(attr, **kwargs): """Serialize TimeDelta object into ISO-8601 formatted string. :param TimeDelta attr: Object to be serialized. :rtype: str - :return: serialized duration """ if isinstance(attr, str): attr = isodate.parse_duration(attr) return isodate.duration_isoformat(attr) @staticmethod - def _serialize_duration_numeric(attr, scale, as_int): - """Serialize a TimeDelta into a numeric value scaled to the wire unit. - - :param TimeDelta attr: Object to be serialized. - :param int scale: Multiplier applied to total seconds (1 for seconds, 1000 for milliseconds). - :param bool as_int: Whether to truncate the result to an int. - :rtype: int or float - :return: serialized duration - """ - if isinstance(attr, str): - attr = isodate.parse_duration(attr) - value = attr.total_seconds() * scale if isinstance(attr, datetime.timedelta) else attr - return int(value) if as_int else float(value) - - @staticmethod - def serialize_duration_seconds_int(attr, **kwargs): # pylint: disable=unused-argument - """Serialize TimeDelta object into an integer number of seconds. - - :param TimeDelta attr: Object to be serialized. - :rtype: int - :return: serialized duration - """ - return Serializer._serialize_duration_numeric(attr, 1, True) - - @staticmethod - def serialize_duration_seconds_float(attr, **kwargs): # pylint: disable=unused-argument - """Serialize TimeDelta object into a floating point number of seconds. - - :param TimeDelta attr: Object to be serialized. - :rtype: float - :return: serialized duration - """ - return Serializer._serialize_duration_numeric(attr, 1, False) - - @staticmethod - def serialize_duration_milliseconds_int(attr, **kwargs): # pylint: disable=unused-argument - """Serialize TimeDelta object into an integer number of milliseconds. - - :param TimeDelta attr: Object to be serialized. - :rtype: int - :return: serialized duration - """ - return Serializer._serialize_duration_numeric(attr, 1000, True) - - @staticmethod - def serialize_duration_milliseconds_float(attr, **kwargs): # pylint: disable=unused-argument - """Serialize TimeDelta object into a floating point number of milliseconds. - - :param TimeDelta attr: Object to be serialized. - :rtype: float - :return: serialized duration - """ - return Serializer._serialize_duration_numeric(attr, 1000, False) - - @staticmethod - def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument + def serialize_rfc(attr, **kwargs): """Serialize Datetime object into RFC-1123 formatted string. :param Datetime attr: Object to be serialized. :rtype: str - :raises TypeError: if format invalid. - :return: serialized rfc + :raises: TypeError if format invalid. """ try: if not attr.tzinfo: _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") utc = attr.utctimetuple() - except AttributeError as exc: - raise TypeError("RFC1123 object must be valid Datetime object.") from exc + except AttributeError: + raise TypeError("RFC1123 object must be valid Datetime object.") return "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT".format( Serializer.days[utc.tm_wday], @@ -1199,13 +1168,12 @@ def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument ) @staticmethod - def serialize_iso(attr, **kwargs): # pylint: disable=unused-argument + def serialize_iso(attr, **kwargs): """Serialize Datetime object into ISO-8601 formatted string. :param Datetime attr: Object to be serialized. :rtype: str - :raises SerializationError: if format invalid. - :return: serialized iso + :raises: SerializationError if format invalid. """ if isinstance(attr, str): attr = isodate.parse_datetime(attr) @@ -1220,25 +1188,29 @@ def serialize_iso(attr, **kwargs): # pylint: disable=unused-argument if microseconds: microseconds = "." + microseconds date = "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}".format( - utc.tm_year, utc.tm_mon, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec + utc.tm_year, + utc.tm_mon, + utc.tm_mday, + utc.tm_hour, + utc.tm_min, + utc.tm_sec, ) return date + microseconds + "Z" except (ValueError, OverflowError) as err: msg = "Unable to serialize datetime object." - raise SerializationError(msg) from err + raise_with_traceback(SerializationError, msg, err) except AttributeError as err: msg = "ISO-8601 object must be valid Datetime object." - raise TypeError(msg) from err + raise_with_traceback(TypeError, msg, err) @staticmethod - def serialize_unix(attr, **kwargs): # pylint: disable=unused-argument + def serialize_unix(attr, **kwargs): """Serialize Datetime object into IntTime format. This is represented as seconds. :param Datetime attr: Object to be serialized. :rtype: int - :raises SerializationError: if format invalid - :return: serialied unix + :raises: SerializationError if format invalid """ if isinstance(attr, int): return attr @@ -1246,17 +1218,17 @@ def serialize_unix(attr, **kwargs): # pylint: disable=unused-argument if not attr.tzinfo: _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") return int(calendar.timegm(attr.utctimetuple())) - except AttributeError as exc: - raise TypeError("Unix time object must be valid Datetime object.") from exc + except AttributeError: + raise TypeError("Unix time object must be valid Datetime object.") -def rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument +def rest_key_extractor(attr, attr_desc, data): key = attr_desc["key"] working_data = data while "." in key: # Need the cast, as for some reasons "split" is typed as list[str | Any] - dict_keys = cast(list[str], _FLATTEN.split(key)) + dict_keys = cast(List[str], _FLATTEN.split(key)) if len(dict_keys) == 1: key = _decode_attribute_map_key(dict_keys[0]) break @@ -1265,15 +1237,14 @@ def rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argumen if working_data is None: # If at any point while following flatten JSON path see None, it means # that all properties under are None as well + # https://github.com/Azure/msrest-for-python/issues/197 return None key = ".".join(dict_keys[1:]) return working_data.get(key) -def rest_key_case_insensitive_extractor( # pylint: disable=unused-argument, inconsistent-return-statements - attr, attr_desc, data -): +def rest_key_case_insensitive_extractor(attr, attr_desc, data): key = attr_desc["key"] working_data = data @@ -1287,6 +1258,7 @@ def rest_key_case_insensitive_extractor( # pylint: disable=unused-argument, inc if working_data is None: # If at any point while following flatten JSON path see None, it means # that all properties under are None as well + # https://github.com/Azure/msrest-for-python/issues/197 return None key = ".".join(dict_keys[1:]) @@ -1294,29 +1266,17 @@ def rest_key_case_insensitive_extractor( # pylint: disable=unused-argument, inc return attribute_key_case_insensitive_extractor(key, None, working_data) -def last_rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - """Extract the attribute in "data" based on the last part of the JSON path key. - - :param str attr: The attribute to extract - :param dict attr_desc: The attribute description - :param dict data: The data to extract from - :rtype: object - :returns: The extracted attribute - """ +def last_rest_key_extractor(attr, attr_desc, data): + """Extract the attribute in "data" based on the last part of the JSON path key.""" key = attr_desc["key"] dict_keys = _FLATTEN.split(key) return attribute_key_extractor(dict_keys[-1], None, data) -def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): # pylint: disable=unused-argument +def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): """Extract the attribute in "data" based on the last part of the JSON path key. This is the case insensitive version of "last_rest_key_extractor" - :param str attr: The attribute to extract - :param dict attr_desc: The attribute description - :param dict data: The data to extract from - :rtype: object - :returns: The extracted attribute """ key = attr_desc["key"] dict_keys = _FLATTEN.split(key) @@ -1353,7 +1313,7 @@ def _extract_name_from_internal_type(internal_type): return xml_name -def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument,too-many-return-statements +def xml_key_extractor(attr, attr_desc, data): if isinstance(data, dict): return None @@ -1405,21 +1365,22 @@ def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument if is_iter_type: if is_wrapped: return None # is_wrapped no node, we want None - return [] # not wrapped, assume empty list + else: + return [] # not wrapped, assume empty list return None # Assume it's not there, maybe an optional node. # If is_iter_type and not wrapped, return all found children if is_iter_type: if not is_wrapped: return children - # Iter and wrapped, should have found one node only (the wrap one) - if len(children) != 1: - raise DeserializationError( - "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( - xml_name + else: # Iter and wrapped, should have found one node only (the wrap one) + if len(children) != 1: + raise DeserializationError( + "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( + xml_name + ) ) - ) - return list(children[0]) # Might be empty list and that's ok. + return list(children[0]) # Might be empty list and that's ok. # Here it's not a itertype, we should have found one element only or empty if len(children) > 1: @@ -1427,7 +1388,7 @@ def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument return children[0] -class Deserializer: +class Deserializer(object): """Response object model deserializer. :param dict classes: Class type dictionary for deserializing complex types. @@ -1436,18 +1397,14 @@ class Deserializer: basic_types = {str: "str", int: "int", bool: "bool", float: "float"} - valid_date = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") + valid_date = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") - def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: + def __init__(self, classes: Optional[Mapping[str, Type[ModelType]]] = None): self.deserialize_type = { "iso-8601": Deserializer.deserialize_iso, "rfc-1123": Deserializer.deserialize_rfc, "unix-time": Deserializer.deserialize_unix, "duration": Deserializer.deserialize_duration, - "duration-seconds-int": Deserializer.deserialize_duration_seconds, - "duration-seconds-float": Deserializer.deserialize_duration_seconds, - "duration-milliseconds-int": Deserializer.deserialize_duration_milliseconds, - "duration-milliseconds-float": Deserializer.deserialize_duration_milliseconds, "date": Deserializer.deserialize_date, "time": Deserializer.deserialize_time, "decimal": Deserializer.deserialize_decimal, @@ -1460,13 +1417,9 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: } self.deserialize_expected_types = { "duration": (isodate.Duration, datetime.timedelta), - "duration-seconds-int": (isodate.Duration, datetime.timedelta), - "duration-seconds-float": (isodate.Duration, datetime.timedelta), - "duration-milliseconds-int": (isodate.Duration, datetime.timedelta), - "duration-milliseconds-float": (isodate.Duration, datetime.timedelta), "iso-8601": (datetime.datetime), } - self.dependencies: dict[str, type] = dict(classes) if classes else {} + self.dependencies: Dict[str, Type[ModelType]] = dict(classes) if classes else {} self.key_extractors = [rest_key_extractor, xml_key_extractor] # Additional properties only works if the "rest_key_extractor" is used to # extract the keys. Making it to work whatever the key extractor is too much @@ -1476,56 +1429,33 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: # Otherwise, result are unexpected self.additional_properties_detection = True - def __call__(self, target_obj, response_data, content_type=None): # pylint: disable=too-many-return-statements + def __call__(self, target_obj, response_data, content_type=None): """Call the deserializer to process a REST response. :param str target_obj: Target data type to deserialize to. :param requests.Response response_data: REST response object. :param str content_type: Swagger "produces" if available. - :raises DeserializationError: if deserialization fails. + :raises: DeserializationError if deserialization fails. :return: Deserialized object. - :rtype: object """ - # Fast path for header deserialization: response_data is a plain str or None - # and target_obj is a simple scalar type. This avoids the expensive - # _unpack_content → _deserialize → _classify_target → deserialize_data chain. - if response_data is None: - return None - if target_obj == "str" and isinstance(response_data, str): - return response_data - if isinstance(response_data, str): - if target_obj == "int": - return int(response_data) - if target_obj == "bool": - if response_data in ("true", "1", "True"): - return True - if response_data in ("false", "0", "False"): - return False - return bool(response_data) - if target_obj == "rfc-1123": - return Deserializer.deserialize_rfc(response_data) - if target_obj == "bytearray": - return Deserializer.deserialize_bytearray(response_data) - data = self._unpack_content(response_data, content_type) return self._deserialize(target_obj, data) - def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return-statements + def _deserialize(self, target_obj, data): """Call the deserializer on a model. Data needs to be already deserialized as JSON or XML ElementTree :param str target_obj: Target data type to deserialize to. :param object data: Object to deserialize. - :raises DeserializationError: if deserialization fails. + :raises: DeserializationError if deserialization fails. :return: Deserialized object. - :rtype: object """ # This is already a model, go recursive just in case if hasattr(data, "_attribute_map"): constants = [name for name, config in getattr(data, "_validation", {}).items() if config.get("constant")] try: - for attr, mapconfig in data._attribute_map.items(): # pylint: disable=protected-access + for attr, mapconfig in data._attribute_map.items(): if attr in constants: continue value = getattr(data, attr) @@ -1542,15 +1472,15 @@ def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return response, class_name = self._classify_target(target_obj, data) - if isinstance(response, str): + if isinstance(response, basestring): return self.deserialize_data(data, response) - if isinstance(response, type) and issubclass(response, Enum): + elif isinstance(response, type) and issubclass(response, Enum): return self.deserialize_enum(data, response) - if data is None or data is CoreNull: + if data is None: return data try: - attributes = response._attribute_map # type: ignore # pylint: disable=protected-access + attributes = response._attribute_map # type: ignore d_attrs = {} for attr, attr_desc in attributes.items(): # Check empty string. If it's not empty, someone has a real "additionalProperties"... @@ -1579,9 +1509,10 @@ def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return d_attrs[attr] = value except (AttributeError, TypeError, KeyError) as err: msg = "Unable to deserialize to object: " + class_name # type: ignore - raise DeserializationError(msg) from err - additional_properties = self._build_additional_properties(attributes, data) - return self._instantiate_model(response, d_attrs, additional_properties) + raise_with_traceback(DeserializationError, msg, err) + else: + additional_properties = self._build_additional_properties(attributes, data) + return self._instantiate_model(response, d_attrs, additional_properties) def _build_additional_properties(self, attribute_map, data): if not self.additional_properties_detection: @@ -1608,20 +1539,18 @@ def _classify_target(self, target, data): :param str target: The target object type to deserialize to. :param str/dict data: The response data to deserialize. - :return: The classified target object and its class name. - :rtype: tuple """ if target is None: return None, None - if isinstance(target, str): + if isinstance(target, basestring): try: target = self.dependencies[target] except KeyError: return target, target try: - target = target._classify(data, self.dependencies) # type: ignore # pylint: disable=protected-access + target = target._classify(data, self.dependencies) except AttributeError: pass # Target is not a Model, no classify return target, target.__class__.__name__ # type: ignore @@ -1636,14 +1565,13 @@ def failsafe_deserialize(self, target_obj, data, content_type=None): :param str target_obj: The target object type to deserialize to. :param str/dict data: The response data to deserialize. :param str content_type: Swagger "produces" if available. - :return: Deserialized object. - :rtype: object """ try: return self(target_obj, data, content_type=content_type) - except: # pylint: disable=bare-except + except: _LOGGER.debug( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", + exc_info=True, ) return None @@ -1659,12 +1587,10 @@ def _unpack_content(raw_data, content_type=None): If raw_data is something else, bypass all logic and return it directly. - :param obj raw_data: Data to be processed. - :param str content_type: How to parse if raw_data is a string/bytes. + :param raw_data: Data to be processed. + :param content_type: How to parse if raw_data is a string/bytes. :raises JSONDecodeError: If JSON is requested and parsing is impossible. :raises UnicodeDecodeError: If bytes is not UTF8 - :rtype: object - :return: Unpacked content. """ # Assume this is enough to detect a Pipeline Response without importing it context = getattr(raw_data, "context", {}) @@ -1681,42 +1607,31 @@ def _unpack_content(raw_data, content_type=None): if hasattr(raw_data, "_content_consumed"): return RawDeserializer.deserialize_from_http_generics(raw_data.text, raw_data.headers) - if isinstance(raw_data, (str, bytes)) or hasattr(raw_data, "read"): + if isinstance(raw_data, (basestring, bytes)) or hasattr(raw_data, "read"): return RawDeserializer.deserialize_from_text(raw_data, content_type) # type: ignore return raw_data def _instantiate_model(self, response, attrs, additional_properties=None): """Instantiate a response model passing in deserialized args. - :param Response response: The response model class. - :param dict attrs: The deserialized response attributes. - :param dict additional_properties: Additional properties to be set. - :rtype: Response - :return: The instantiated response model. + :param response: The response model class. + :param d_attrs: The deserialized response attributes. """ if callable(response): subtype = getattr(response, "_subtype_map", {}) try: - readonly = [ - k - for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore - if v.get("readonly") - ] - const = [ - k - for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore - if v.get("constant") - ] + readonly = [k for k, v in response._validation.items() if v.get("readonly")] + const = [k for k, v in response._validation.items() if v.get("constant")] kwargs = {k: v for k, v in attrs.items() if k not in subtype and k not in readonly + const} response_obj = response(**kwargs) for attr in readonly: setattr(response_obj, attr, attrs.get(attr)) if additional_properties: - response_obj.additional_properties = additional_properties # type: ignore + response_obj.additional_properties = additional_properties return response_obj except TypeError as err: msg = "Unable to deserialize {} into model {}. ".format(kwargs, response) # type: ignore - raise DeserializationError(msg + str(err)) from err + raise DeserializationError(msg + str(err)) else: try: for attr, value in attrs.items(): @@ -1725,16 +1640,15 @@ def _instantiate_model(self, response, attrs, additional_properties=None): except Exception as exp: msg = "Unable to populate response model. " msg += "Type: {}, Error: {}".format(type(response), exp) - raise DeserializationError(msg) from exp + raise DeserializationError(msg) - def deserialize_data(self, data, data_type): # pylint: disable=too-many-return-statements + def deserialize_data(self, data, data_type): """Process data for deserialization according to data type. :param str data: The response string to be deserialized. :param str data_type: The type to deserialize to. - :raises DeserializationError: if deserialization fails. + :raises: DeserializationError if deserialization fails. :return: Deserialized object. - :rtype: object """ if data is None: return data @@ -1748,11 +1662,7 @@ def deserialize_data(self, data, data_type): # pylint: disable=too-many-return- if isinstance(data, self.deserialize_expected_types.get(data_type, tuple())): return data - is_a_text_parsing_type = lambda x: x not in [ # pylint: disable=unnecessary-lambda-assignment - "object", - "[]", - r"{}", - ] + is_a_text_parsing_type = lambda x: x not in ["object", "[]", r"{}"] if isinstance(data, ET.Element) and is_a_text_parsing_type(data_type) and not data.text: return None data_val = self.deserialize_type[data_type](data) @@ -1771,15 +1681,15 @@ def deserialize_data(self, data, data_type): # pylint: disable=too-many-return- except (ValueError, TypeError, AttributeError) as err: msg = "Unable to deserialize response data." msg += " Data: {}, {}".format(data, data_type) - raise DeserializationError(msg) from err - return self._deserialize(obj_type, data) + raise_with_traceback(DeserializationError, msg, err) + else: + return self._deserialize(obj_type, data) def deserialize_iter(self, attr, iter_type): """Deserialize an iterable. :param list attr: Iterable to be deserialized. :param str iter_type: The type of object in the iterable. - :return: Deserialized iterable. :rtype: list """ if attr is None: @@ -1796,7 +1706,6 @@ def deserialize_dict(self, attr, dict_type): :param dict/list attr: Dictionary to be deserialized. Also accepts a list of key, value pairs. :param str dict_type: The object type of the items in the dictionary. - :return: Deserialized dictionary. :rtype: dict """ if isinstance(attr, list): @@ -1807,21 +1716,20 @@ def deserialize_dict(self, attr, dict_type): attr = {el.tag: el.text for el in attr} return {k: self.deserialize_data(v, dict_type) for k, v in attr.items()} - def deserialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements + def deserialize_object(self, attr, **kwargs): """Deserialize a generic object. This will be handled as a dictionary. :param dict attr: Dictionary to be deserialized. - :return: Deserialized object. :rtype: dict - :raises TypeError: if non-builtin datatype encountered. + :raises: TypeError if non-builtin datatype encountered. """ if attr is None: return None if isinstance(attr, ET.Element): # Do no recurse on XML, just return the tree as-is return attr - if isinstance(attr, str): + if isinstance(attr, basestring): return self.deserialize_basic(attr, "str") obj_type = type(attr) if obj_type in self.basic_types: @@ -1847,10 +1755,11 @@ def deserialize_object(self, attr, **kwargs): # pylint: disable=too-many-return pass return deserialized - error = "Cannot deserialize generic object with type: " - raise TypeError(error + str(obj_type)) + else: + error = "Cannot deserialize generic object with type: " + raise TypeError(error + str(obj_type)) - def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return-statements + def deserialize_basic(self, attr, data_type): """Deserialize basic builtin data type from string. Will attempt to convert to str, int, float and bool. This function will also accept '1', '0', 'true' and 'false' as @@ -1858,9 +1767,8 @@ def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return :param str attr: response string to be deserialized. :param str data_type: deserialization data type. - :return: Deserialized basic type. :rtype: str, int, float or bool - :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool. + :raises: TypeError if string format is not valid. """ # If we're here, data is supposed to be a basic type. # If it's still an XML node, take the text @@ -1870,27 +1778,24 @@ def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return if data_type == "str": # None or '', node is empty string. return "" - # None or '', node with a strong type is None. - # Don't try to model "empty bool" or "empty int" - return None + else: + # None or '', node with a strong type is None. + # Don't try to model "empty bool" or "empty int" + return None if data_type == "bool": if attr in [True, False, 1, 0]: return bool(attr) - if isinstance(attr, str): + elif isinstance(attr, basestring): if attr.lower() in ["true", "1"]: return True - if attr.lower() in ["false", "0"]: + elif attr.lower() in ["false", "0"]: return False raise TypeError("Invalid boolean value: {}".format(attr)) if data_type == "str": return self.deserialize_unicode(attr) - if data_type == "int": - return int(attr) - if data_type == "float": - return float(attr) - raise TypeError("Unknown basic data type: {}".format(data_type)) + return eval(data_type)(attr) # nosec @staticmethod def deserialize_unicode(data): @@ -1898,7 +1803,6 @@ def deserialize_unicode(data): as a string. :param str data: response string to be deserialized. - :return: Deserialized string. :rtype: str or unicode """ # We might be here because we have an enum modeled as string, @@ -1912,7 +1816,8 @@ def deserialize_unicode(data): return data except NameError: return str(data) - return str(data) + else: + return str(data) @staticmethod def deserialize_enum(data, enum_obj): @@ -1924,7 +1829,6 @@ def deserialize_enum(data, enum_obj): :param str data: Response string to be deserialized. If this value is None or invalid it will be returned as-is. :param Enum enum_obj: Enum object to deserialize to. - :return: Deserialized enum object. :rtype: Enum """ if isinstance(data, enum_obj) or data is None: @@ -1933,11 +1837,12 @@ def deserialize_enum(data, enum_obj): data = data.value if isinstance(data, int): # Workaround. We might consider remove it in the future. + # https://github.com/Azure/azure-rest-api-specs/issues/141 try: return list(enum_obj.__members__.values())[data] - except IndexError as exc: + except IndexError: error = "{!r} is not a valid index for enum {!r}" - raise DeserializationError(error.format(data, enum_obj)) from exc + raise DeserializationError(error.format(data, enum_obj)) try: return enum_obj(str(data)) except ValueError: @@ -1945,7 +1850,11 @@ def deserialize_enum(data, enum_obj): if enum_value.value.lower() == str(data).lower(): return enum_value # We don't fail anymore for unknown value, we deserialize as a string - _LOGGER.warning("Deserializer is not able to find %s as valid enum in %s", data, enum_obj) + _LOGGER.warning( + "Deserializer is not able to find %s as valid enum in %s", + data, + enum_obj, + ) return Deserializer.deserialize_unicode(data) @staticmethod @@ -1953,9 +1862,8 @@ def deserialize_bytearray(attr): """Deserialize string into bytearray. :param str attr: response string to be deserialized. - :return: Deserialized bytearray :rtype: bytearray - :raises TypeError: if string format invalid. + :raises: TypeError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1966,9 +1874,8 @@ def deserialize_base64(attr): """Deserialize base64 encoded string into string. :param str attr: response string to be deserialized. - :return: Deserialized base64 string :rtype: bytearray - :raises TypeError: if string format invalid. + :raises: TypeError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1982,26 +1889,24 @@ def deserialize_decimal(attr): """Deserialize string into Decimal object. :param str attr: response string to be deserialized. - :return: Deserialized decimal - :raises DeserializationError: if string format invalid. - :rtype: decimal + :rtype: Decimal + :raises: DeserializationError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text try: - return decimal.Decimal(str(attr)) # type: ignore + return decimal.Decimal(attr) # type: ignore except decimal.DecimalException as err: msg = "Invalid decimal {}".format(attr) - raise DeserializationError(msg) from err + raise_with_traceback(DeserializationError, msg, err) @staticmethod def deserialize_long(attr): """Deserialize string into long (Py2) or int (Py3). :param str attr: response string to be deserialized. - :return: Deserialized int :rtype: long or int - :raises ValueError: if string format invalid. + :raises: ValueError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2012,9 +1917,8 @@ def deserialize_duration(attr): """Deserialize ISO-8601 formatted string into TimeDelta object. :param str attr: response string to be deserialized. - :return: Deserialized duration :rtype: TimeDelta - :raises DeserializationError: if string format invalid. + :raises: DeserializationError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2022,59 +1926,17 @@ def deserialize_duration(attr): duration = isodate.parse_duration(attr) except (ValueError, OverflowError, AttributeError) as err: msg = "Cannot deserialize duration object." - raise DeserializationError(msg) from err - return duration - - @staticmethod - def _deserialize_duration_numeric(attr, unit): - """Deserialize a numeric duration value into a TimeDelta object. - - :param float attr: response value to be deserialized. - :param str unit: The wire unit, used as the ``timedelta`` keyword - (``"seconds"`` or ``"milliseconds"``). - :return: Deserialized duration - :rtype: TimeDelta - :raises DeserializationError: if value is invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - duration = datetime.timedelta(**{unit: float(attr)}) # type: ignore - except (ValueError, OverflowError, TypeError) as err: - msg = "Cannot deserialize duration object." - raise DeserializationError(msg) from err - return duration - - @staticmethod - def deserialize_duration_seconds(attr): - """Deserialize a numeric number of seconds into a TimeDelta object. - - :param float attr: response value to be deserialized. - :return: Deserialized duration - :rtype: TimeDelta - :raises DeserializationError: if value is invalid. - """ - return Deserializer._deserialize_duration_numeric(attr, "seconds") - - @staticmethod - def deserialize_duration_milliseconds(attr): - """Deserialize a numeric number of milliseconds into a TimeDelta object. - - :param float attr: response value to be deserialized. - :return: Deserialized duration - :rtype: TimeDelta - :raises DeserializationError: if value is invalid. - """ - return Deserializer._deserialize_duration_numeric(attr, "milliseconds") + raise_with_traceback(DeserializationError, msg, err) + else: + return duration @staticmethod def deserialize_date(attr): """Deserialize ISO-8601 formatted string into Date object. :param str attr: response string to be deserialized. - :return: Deserialized date :rtype: Date - :raises DeserializationError: if string format invalid. + :raises: DeserializationError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2088,9 +1950,8 @@ def deserialize_time(attr): """Deserialize ISO-8601 formatted string into time object. :param str attr: response string to be deserialized. - :return: Deserialized time :rtype: datetime.time - :raises DeserializationError: if string format invalid. + :raises: DeserializationError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2103,32 +1964,31 @@ def deserialize_rfc(attr): """Deserialize RFC-1123 formatted string into Datetime object. :param str attr: response string to be deserialized. - :return: Deserialized RFC datetime :rtype: Datetime - :raises DeserializationError: if string format invalid. + :raises: DeserializationError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text try: parsed_date = email.utils.parsedate_tz(attr) # type: ignore date_obj = datetime.datetime( - *parsed_date[:6], tzinfo=datetime.timezone(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) + *parsed_date[:6], tzinfo=_FixedOffset(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) ) if not date_obj.tzinfo: date_obj = date_obj.astimezone(tz=TZ_UTC) except ValueError as err: msg = "Cannot deserialize to rfc datetime object." - raise DeserializationError(msg) from err - return date_obj + raise_with_traceback(DeserializationError, msg, err) + else: + return date_obj @staticmethod def deserialize_iso(attr): """Deserialize ISO-8601 formatted string into Datetime object. :param str attr: response string to be deserialized. - :return: Deserialized ISO datetime :rtype: Datetime - :raises DeserializationError: if string format invalid. + :raises: DeserializationError if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2155,8 +2015,9 @@ def deserialize_iso(attr): raise OverflowError("Hit max or min date") except (ValueError, OverflowError, AttributeError) as err: msg = "Cannot deserialize datetime object." - raise DeserializationError(msg) from err - return date_obj + raise_with_traceback(DeserializationError, msg, err) + else: + return date_obj @staticmethod def deserialize_unix(attr): @@ -2164,16 +2025,15 @@ def deserialize_unix(attr): This is represented as seconds. :param int attr: Object to be serialized. - :return: Deserialized datetime :rtype: Datetime - :raises DeserializationError: if format invalid + :raises: DeserializationError if format invalid """ if isinstance(attr, ET.Element): attr = int(attr.text) # type: ignore try: - attr = int(attr) date_obj = datetime.datetime.fromtimestamp(attr, TZ_UTC) except ValueError as err: msg = "Cannot deserialize to unix datetime object." - raise DeserializationError(msg) from err - return date_obj + raise_with_traceback(DeserializationError, msg, err) + else: + return date_obj diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py deleted file mode 100644 index 8026245c2abc..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py deleted file mode 100644 index 35d5fc024978..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_utils/model_base.py +++ /dev/null @@ -1,1787 +0,0 @@ -# pylint: disable=line-too-long,useless-suppression,too-many-lines -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- -# pylint: disable=protected-access, broad-except - -import copy -import calendar -import decimal -import functools -import sys -import logging -import base64 -import re -import typing -import enum -import email.utils -from datetime import datetime, date, time, timedelta, timezone -from json import JSONEncoder -import xml.etree.ElementTree as ET -from collections.abc import MutableMapping -import isodate -from azure.core.exceptions import DeserializationError -from azure.core import CaseInsensitiveEnumMeta -from azure.core.pipeline import PipelineResponse -from azure.core.serialization import _Null - -from azure.core.rest import HttpResponse - -if sys.version_info >= (3, 11): - from typing import Self -else: - from typing_extensions import Self - -_LOGGER = logging.getLogger(__name__) - -__all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] - -TZ_UTC = timezone.utc -_T = typing.TypeVar("_T") -_NONE_TYPE = type(None) - - -def _timedelta_as_isostr(td: timedelta) -> str: - """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S' - - Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython - - :param timedelta td: The timedelta to convert - :rtype: str - :return: ISO8601 version of this timedelta - """ - - # Split seconds to larger units - seconds = td.total_seconds() - minutes, seconds = divmod(seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - - days, hours, minutes = list(map(int, (days, hours, minutes))) - seconds = round(seconds, 6) - - # Build date - date_str = "" - if days: - date_str = "%sD" % days - - if hours or minutes or seconds: - # Build time - time_str = "T" - - # Hours - bigger_exists = date_str or hours - if bigger_exists: - time_str += "{:02}H".format(hours) - - # Minutes - bigger_exists = bigger_exists or minutes - if bigger_exists: - time_str += "{:02}M".format(minutes) - - # Seconds - try: - if seconds.is_integer(): - seconds_string = "{:02}".format(int(seconds)) - else: - # 9 chars long w/ leading 0, 6 digits after decimal - seconds_string = "%09.6f" % seconds - # Remove trailing zeros - seconds_string = seconds_string.rstrip("0") - except AttributeError: # int.is_integer() raises - seconds_string = "{:02}".format(seconds) - - time_str += "{}S".format(seconds_string) - else: - time_str = "" - - return "P" + date_str + time_str - - -def _serialize_bytes(o, format: typing.Optional[str] = None) -> str: - encoded = base64.b64encode(o).decode() - if format == "base64url": - return encoded.strip("=").replace("+", "-").replace("/", "_") - return encoded - - -def _serialize_duration(td: timedelta, format: typing.Optional[str] = None): - """Serialize a timedelta to its wire representation. - - For the ``seconds``/``milliseconds`` encodings the value is converted to a - numeric value, otherwise it falls back to an ISO 8601 duration string. - - :param timedelta td: The timedelta to serialize. - :param str format: The duration encoding format. - :rtype: int or float or str - :return: serialized duration - """ - seconds = td.total_seconds() - if format == "duration-seconds-int": - return int(seconds) - if format == "duration-seconds-float": - return seconds - if format == "duration-milliseconds-int": - return int(seconds * 1000) - if format == "duration-milliseconds-float": - return seconds * 1000 - return _timedelta_as_isostr(td) - - -def _serialize_datetime(o, format: typing.Optional[str] = None): - if hasattr(o, "year") and hasattr(o, "hour"): - if format == "rfc7231": - return email.utils.format_datetime(o, usegmt=True) - if format == "unix-timestamp": - return int(calendar.timegm(o.utctimetuple())) - - # astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set) - if not o.tzinfo: - iso_formatted = o.replace(tzinfo=TZ_UTC).isoformat() - else: - iso_formatted = o.astimezone(TZ_UTC).isoformat() - # Replace the trailing "+00:00" UTC offset with "Z" (RFC 3339: https://www.ietf.org/rfc/rfc3339.txt) - return iso_formatted.replace("+00:00", "Z") - # Next try datetime.date or datetime.time - return o.isoformat() - - -def _is_readonly(p): - try: - return p._visibility == ["read"] - except AttributeError: - return False - - -class SdkJSONEncoder(JSONEncoder): - """A JSON encoder that's capable of serializing datetime objects and bytes. - - :param args: Additional positional arguments passed to the base ``JSONEncoder``. - :type args: typing.Any - :keyword exclude_readonly: Whether to exclude readonly properties. Defaults to False. - :paramtype exclude_readonly: bool - :keyword format: The format to use for serialization. Defaults to None. - :paramtype format: typing.Optional[str] - """ - - def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs): - super().__init__(*args, **kwargs) - self.exclude_readonly = exclude_readonly - self.format = format - - def default(self, o): # pylint: disable=too-many-return-statements - if _is_model(o): - if self.exclude_readonly: - readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] - return {k: v for k, v in o.items() if k not in readonly_props} - return dict(o.items()) - try: - return super(SdkJSONEncoder, self).default(o) - except TypeError: - if isinstance(o, _Null): - return None - if isinstance(o, decimal.Decimal): - return float(o) - if isinstance(o, (bytes, bytearray)): - return _serialize_bytes(o, self.format) - try: - # First try datetime.datetime - return _serialize_datetime(o, self.format) - except AttributeError: - pass - # Last, try datetime.timedelta - try: - return _timedelta_as_isostr(o) - except AttributeError: - # This will be raised when it hits value.total_seconds in the method above - pass - return super(SdkJSONEncoder, self).default(o) - - -_VALID_DATE = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" + r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") -_VALID_RFC7231 = re.compile( - r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s" - r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT" -) - -_ARRAY_ENCODE_MAPPING = { - "pipeDelimited": "|", - "spaceDelimited": " ", - "commaDelimited": ",", - "newlineDelimited": "\n", -} - - -def _deserialize_array_encoded(delimit: str, attr): - if isinstance(attr, str): - if attr == "": - return [] - return attr.split(delimit) - return attr - - -def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: - """Deserialize ISO-8601 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - attr = attr.upper() - match = _VALID_DATE.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - check_decimal = attr.split(".") - if len(check_decimal) > 1: - decimal_str = "" - for digit in check_decimal[1]: - if digit.isdigit(): - decimal_str += digit - else: - break - if len(decimal_str) > 6: - attr = attr.replace(decimal_str, decimal_str[0:6]) - - date_obj = isodate.parse_datetime(attr) - test_utc = date_obj.utctimetuple() - if test_utc.tm_year > 9999 or test_utc.tm_year < 1: - raise OverflowError("Hit max or min date") - return date_obj # type: ignore[no-any-return] - - -def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime: - """Deserialize RFC7231 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - match = _VALID_RFC7231.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - return email.utils.parsedate_to_datetime(attr) - - -def _deserialize_datetime_unix_timestamp(attr: typing.Union[float, datetime]) -> datetime: - """Deserialize unix timestamp into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - return datetime.fromtimestamp(attr, TZ_UTC) - - -def _deserialize_date(attr: typing.Union[str, date]) -> date: - """Deserialize ISO-8601 formatted string into Date object. - :param str attr: response string to be deserialized. - :rtype: date - :returns: The date object from that input - """ - # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. - if isinstance(attr, date): - return attr - return isodate.parse_date(attr, defaultmonth=None, defaultday=None) # type: ignore - - -def _deserialize_time(attr: typing.Union[str, time]) -> time: - """Deserialize ISO-8601 formatted string into time object. - - :param str attr: response string to be deserialized. - :rtype: datetime.time - :returns: The time object from that input - """ - if isinstance(attr, time): - return attr - return isodate.parse_time(attr) # type: ignore[no-any-return] - - -def _deserialize_bytes(attr): - if isinstance(attr, (bytes, bytearray)): - return attr - return bytes(base64.b64decode(attr)) - - -def _deserialize_bytes_base64(attr): - if isinstance(attr, (bytes, bytearray)): - return attr - padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore - attr = attr + padding # type: ignore - encoded = attr.replace("-", "+").replace("_", "/") - return bytes(base64.b64decode(encoded)) - - -def _deserialize_duration(attr): - if isinstance(attr, timedelta): - return attr - return isodate.parse_duration(attr) - - -def _deserialize_duration_numeric(attr, unit): - if isinstance(attr, timedelta): - return attr - return timedelta(**{unit: float(attr)}) - - -def _deserialize_decimal(attr): - if isinstance(attr, decimal.Decimal): - return attr - return decimal.Decimal(str(attr)) - - -def _deserialize_int_as_str(attr): - if isinstance(attr, int): - return attr - return int(attr) - - -def _deserialize_bool_as_str(attr): - if isinstance(attr, bool): - return attr - return attr.lower() == "true" - - -_DESERIALIZE_MAPPING = { - datetime: _deserialize_datetime, - date: _deserialize_date, - time: _deserialize_time, - bytes: _deserialize_bytes, - bytearray: _deserialize_bytes, - timedelta: _deserialize_duration, - typing.Any: lambda x: x, - decimal.Decimal: _deserialize_decimal, -} - -_DESERIALIZE_MAPPING_WITHFORMAT = { - "rfc3339": _deserialize_datetime, - "rfc7231": _deserialize_datetime_rfc7231, - "unix-timestamp": _deserialize_datetime_unix_timestamp, - "base64": _deserialize_bytes, - "base64url": _deserialize_bytes_base64, - "duration-seconds-int": functools.partial(_deserialize_duration_numeric, unit="seconds"), - "duration-seconds-float": functools.partial(_deserialize_duration_numeric, unit="seconds"), - "duration-milliseconds-int": functools.partial(_deserialize_duration_numeric, unit="milliseconds"), - "duration-milliseconds-float": functools.partial(_deserialize_duration_numeric, unit="milliseconds"), -} - - -def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None): - if annotation is int and rf and rf._format == "str": - return _deserialize_int_as_str - if annotation is bool and rf and rf._format == "str": - return _deserialize_bool_as_str - if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING: - return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format]) - if rf and rf._format: - return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) - return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore - - -def _get_type_alias_type(module_name: str, alias_name: str): - types = { - k: v - for k, v in sys.modules[module_name].__dict__.items() - if isinstance(v, typing._GenericAlias) # type: ignore - } - if alias_name not in types: - return alias_name - return types[alias_name] - - -def _get_model(module_name: str, model_name: str): - models = {k: v for k, v in sys.modules[module_name].__dict__.items() if isinstance(v, type)} - module_end = module_name.rsplit(".", 1)[0] - models.update({k: v for k, v in sys.modules[module_end].__dict__.items() if isinstance(v, type)}) - if isinstance(model_name, str): - model_name = model_name.split(".")[-1] - if model_name not in models: - return model_name - return models[model_name] - - -_UNSET = object() - - -class _MyMutableMapping(MutableMapping[str, typing.Any]): - def __init__(self, data: dict[str, typing.Any]) -> None: - self._data = data - - def __contains__(self, key: typing.Any) -> bool: - return key in self._data - - def __getitem__(self, key: str) -> typing.Any: - # If this key has been deserialized (for mutable types), we need to handle serialization - if hasattr(self, "_attr_to_rest_field"): - cache_attr = f"_deserialized_{key}" - if hasattr(self, cache_attr): - rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key) - if rf: - value = self._data.get(key) - if isinstance(value, (dict, list, set)): - # For mutable types, serialize and return - # But also update _data with serialized form and clear flag - # so mutations via this returned value affect _data - serialized = _serialize(value, rf._format) - # If serialized form is same type (no transformation needed), - # return _data directly so mutations work - if isinstance(serialized, type(value)) and serialized == value: - return self._data.get(key) - # Otherwise return serialized copy and clear flag - try: - object.__delattr__(self, cache_attr) - except AttributeError: - pass - # Store serialized form back - self._data[key] = serialized - return serialized - return self._data.__getitem__(key) - - def __setitem__(self, key: str, value: typing.Any) -> None: - # Clear any cached deserialized value when setting through dictionary access - cache_attr = f"_deserialized_{key}" - try: - object.__delattr__(self, cache_attr) - except AttributeError: - pass - self._data.__setitem__(key, value) - - def __delitem__(self, key: str) -> None: - self._data.__delitem__(key) - - def __iter__(self) -> typing.Iterator[typing.Any]: - return self._data.__iter__() - - def __len__(self) -> int: - return self._data.__len__() - - def __ne__(self, other: typing.Any) -> bool: - return not self.__eq__(other) - - def keys(self) -> typing.KeysView[str]: - """ - :returns: a set-like object providing a view on the mapping's keys - :rtype: ~typing.KeysView - """ - return self._data.keys() - - def values(self) -> typing.ValuesView[typing.Any]: - """ - :returns: an object providing a view on the mapping's values - :rtype: ~typing.ValuesView - """ - return self._data.values() - - def items(self) -> typing.ItemsView[str, typing.Any]: - """ - :returns: a set-like object providing a view on the mapping's items - :rtype: ~typing.ItemsView - """ - return self._data.items() - - def get(self, key: str, default: typing.Any = None) -> typing.Any: - """ - Get the value for key if key is in the dictionary, else default. - :param str key: The key to look up. - :param any default: The value to return if key is not in the dictionary. Defaults to None - :returns: The value for key if key is in the dictionary, else default. - :rtype: any - """ - try: - return self[key] - except KeyError: - return default - - @typing.overload - def pop(self, key: str) -> typing.Any: ... # pylint: disable=arguments-differ - - @typing.overload - def pop(self, key: str, default: _T) -> _T: ... # pylint: disable=signature-differs - - @typing.overload - def pop(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs - - def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any: - """ - Removes specified key and return the corresponding value. - :param str key: The key to pop. - :param any default: The value to return if key is not in the dictionary - :returns: The value corresponding to the key. - :rtype: any - :raises KeyError: If key is not found and default is not given. - """ - if default is _UNSET: - return self._data.pop(key) - return self._data.pop(key, default) - - def popitem(self) -> tuple[str, typing.Any]: - """ - Removes and returns some (key, value) pair - :returns: The (key, value) pair. - :rtype: tuple - :raises KeyError: if the dictionary is empty. - """ - return self._data.popitem() - - def clear(self) -> None: - """ - Remove all items from the dictionary. - """ - self._data.clear() - - def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: # pylint: disable=arguments-differ - """ - Update the dictionary from a mapping or an iterable of key-value pairs. - :param any args: Either a mapping object or an iterable of key-value pairs. - """ - self._data.update(*args, **kwargs) - - @typing.overload - def setdefault(self, key: str, default: None = None) -> None: ... - - @typing.overload - def setdefault(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs - - def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any: - """ - Return the value for key if key is in the dictionary; otherwise set the key to - default and return default. - :param str key: The key to look up. - :param any default: The value to set if key is not in the dictionary - :returns: The value for key if key is in the dictionary, else default. - :rtype: any - """ - if default is _UNSET: - return self._data.setdefault(key) - return self._data.setdefault(key, default) - - def __eq__(self, other: typing.Any) -> bool: - if isinstance(other, _MyMutableMapping): - return self._data == other._data - try: - other_model = self.__class__(other) - except Exception: - return False - return self._data == other_model._data - - def __repr__(self) -> str: - return str(self._data) - - -def _is_model(obj: typing.Any) -> bool: - return getattr(obj, "_is_model", False) - - -def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements - if isinstance(o, list): - if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o): - return _ARRAY_ENCODE_MAPPING[format].join(o) - return [_serialize(x, format) for x in o] - if isinstance(o, dict): - return {k: _serialize(v, format) for k, v in o.items()} - if isinstance(o, set): - return {_serialize(x, format) for x in o} - if isinstance(o, tuple): - return tuple(_serialize(x, format) for x in o) - if isinstance(o, (bytes, bytearray)): - return _serialize_bytes(o, format) - if isinstance(o, decimal.Decimal): - return float(o) - if isinstance(o, enum.Enum): - return o.value - if isinstance(o, int): - if format == "str": - return str(o) - return o - try: - # First try datetime.datetime - return _serialize_datetime(o, format) - except AttributeError: - pass - # Last, try datetime.timedelta - try: - return _serialize_duration(o, format) - except AttributeError: - # This will be raised when it hits value.total_seconds in the method above - pass - return o - - -def _get_rest_field(attr_to_rest_field: dict[str, "_RestField"], rest_name: str) -> typing.Optional["_RestField"]: - try: - return next(rf for rf in attr_to_rest_field.values() if rf._rest_name == rest_name) - except StopIteration: - return None - - -def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any: - if not rf: - return _serialize(value, None) - if rf._is_multipart_file_input: - return value - if rf._is_model: - return _deserialize(rf._type, value) - if isinstance(value, ET.Element): - value = _deserialize(rf._type, value) - return _serialize(value, rf._format) - - -# ============================================================================ -# Fast-path scalar deserializer functions for rest_field(deserializer=...) -# These are referenced from rest_field declarations to bypass the generic -# _deserialize -> _deserialize_with_callable chain. -# Only simple/primitive types — no models or container types. -# ============================================================================ - - -def _xml_deser_str(value): - if isinstance(value, ET.Element): - return value.text or "" - return str(value) if value is not None else None - - -def _xml_deser_int(value): - if isinstance(value, ET.Element): - return int(value.text) if value.text else None - return int(value) if value is not None else None - - -def _xml_deser_float(value): - if isinstance(value, ET.Element): - return float(value.text) if value.text else None - return float(value) if value is not None else None - - -def _xml_deser_bool(value): - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - if text in (True, False): - return text - return text.lower() == "true" - - -# pylint: disable=docstring-missing-param -def _xml_deser_bytes(value): - """Deserialize bytes from XML (base64).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_bytes(text) - - -def _xml_deser_bytes_base64url(value): - """Deserialize bytes from XML (base64url).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_bytes_base64(text) - - -def _xml_deser_datetime(value): - """Deserialize a datetime from XML (ISO 8601 / rfc3339).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_datetime(text) - - -def _xml_deser_datetime_rfc7231(value): - """Deserialize a datetime from XML (RFC7231 format).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_datetime_rfc7231(text) - - -def _xml_deser_datetime_unix_timestamp(value): - """Deserialize a datetime from XML (Unix timestamp).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_datetime_unix_timestamp(float(text)) - - -def _xml_deser_date(value): - """Deserialize a date from XML (ISO 8601).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_date(text) - - -def _xml_deser_time(value): - """Deserialize a time from XML (ISO 8601).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_time(text) - - -def _xml_deser_duration(value): - """Deserialize a timedelta from XML (ISO 8601 duration).""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_duration(text) - - -def _xml_deser_decimal(value): - """Deserialize a Decimal from XML.""" - if isinstance(value, ET.Element): - text = value.text - else: - text = value - if text is None: - return None - return _deserialize_decimal(text) - - -def _xml_deser_enum_or_str(enum_cls, value): - """Deserialize a Union[EnumType, str] from XML.""" - text = value.text if isinstance(value, ET.Element) else value - if text is None: - return None - try: - return enum_cls(text) - except ValueError: - return text - - -def _extract_xml_model_type(rf_type): - """Extract the concrete Model class from a resolved rf._type partial chain. - - Unwraps ``Optional[Model]`` and ``_deserialize_model(Model, ...)`` - wrappers. Only handles Model and Optional[Model] — other composite - types (List, Dict, Union, etc.) return None and fall through to the - generic ``_deserialize`` path at runtime. - """ - if rf_type is None: - return None - if isinstance(rf_type, type) and _is_model(rf_type): - return rf_type - if not isinstance(rf_type, functools.partial): - return None - func = rf_type.func - args = rf_type.args - if func is _deserialize_with_optional and args: - return _extract_xml_model_type(args[0]) - if func is _deserialize_model and args: - cls = args[0] - return cls if isinstance(cls, type) and _is_model(cls) else None - return None - - -def _build_xml_field_plan( # pylint: disable=docstring-missing-return, docstring-missing-rtype, unused-variable - cls, attr_to_rest_field: dict -) -> list: - """Build a precomputed XML field plan for fast _init_from_xml iteration. - - Called once per model class in __new__. Returns a list of tuples: - (rest_name, xml_name, kind, deser, rf_type, is_optional, items_name) - - kind: 0=wrapped, 1=attribute, 2=unwrapped, 3=text - - For Model and Optional[Model] fields that lack a scalar - ``_deserializer``, this function precomputes the Model class as the - deserializer so ``_init_from_xml`` can call ``ModelClass(element)`` - directly instead of going through the expensive - ``_get_deserialize_callable_from_annotation`` chain at runtime. - """ - model_meta = getattr(cls, "_xml", {}) - model_ns = model_meta.get("ns") or model_meta.get("namespace") - plan = [] - - for rf in attr_to_rest_field.values(): - prop_meta = getattr(rf, "_xml", {}) - deser = rf._deserializer - - xml_name = prop_meta.get("name", rf._rest_name) - xml_ns = _resolve_xml_ns(prop_meta, model_meta) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - is_optional = rf._is_optional - - # For Model / Optional[Model] fields without a scalar deserializer, - # precompute the Model class as the deserializer. - if deser is None and rf._type is not None: - model_cls = _extract_xml_model_type(rf._type) - if model_cls is not None: - deser = model_cls - - if prop_meta.get("attribute", False): - plan.append((rf._rest_name, xml_name, 1, deser, rf._type, is_optional, None)) - elif prop_meta.get("unwrapped", False): - items_name = prop_meta.get("itemsName") - if items_name: - items_ns = prop_meta.get("itemsNs") - if items_ns is not None: - xml_ns = items_ns - if xml_ns: - items_name = "{" + xml_ns + "}" + items_name - else: - items_name = xml_name - plan.append((rf._rest_name, xml_name, 2, deser, rf._type, is_optional, items_name)) - elif prop_meta.get("text", False): - plan.append((rf._rest_name, xml_name, 3, deser, rf._type, is_optional, None)) - else: - plan.append((rf._rest_name, xml_name, 0, deser, rf._type, is_optional, None)) - - return plan - - -# pylint: enable=docstring-missing-param -class Model(_MyMutableMapping): - _is_model = True - # label whether current class's _attr_to_rest_field has been calculated - # could not see _attr_to_rest_field directly because subclass inherits it from parent class - _calculated: set[str] = set() - - def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: - class_name = self.__class__.__name__ - if len(args) > 1: - raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") - dict_to_pass: dict[str, typing.Any] = {} - if args: - if isinstance(args[0], ET.Element): - dict_to_pass.update(self._init_from_xml(args[0])) - else: - dict_to_pass.update( - {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()} - ) - else: - non_attr_kwargs = [k for k in kwargs if k not in self._attr_to_rest_field] - if non_attr_kwargs: - # actual type errors only throw the first wrong keyword arg they see, so following that. - raise TypeError(f"{class_name}.__init__() got an unexpected keyword argument '{non_attr_kwargs[0]}'") - dict_to_pass.update( - { - self._attr_to_rest_field[k]._rest_name: _create_value(self._attr_to_rest_field[k], v) - for k, v in kwargs.items() - if v is not None - } - ) - # Apply client default values for fields the caller didn't set so that - # defaults are part of `_data` and therefore included during serialization. - for rf in self._attr_to_rest_field.values(): - if rf._default is _UNSET: - continue - if rf._rest_name in dict_to_pass: - continue - dict_to_pass[rf._rest_name] = _create_value(rf, rf._default) - super().__init__(dict_to_pass) - - def _init_from_xml( # pylint: disable=too-many-branches, too-many-statements - self, element: ET.Element - ) -> dict[str, typing.Any]: - """Deserialize an XML element into a dict mapping rest field names to values. - - :param ET.Element element: The XML element to deserialize from. - :returns: A dictionary of rest_name to deserialized value pairs. - :rtype: dict - """ - result: dict[str, typing.Any] = {} - existed_attr_keys: list[str] = [] - - field_plan = getattr(self, "_xml_field_plan", None) - if field_plan: - for rest_name, xml_name, kind, deser, rf_type, is_optional, items_name in field_plan: - if kind == 0: # wrapped element (most common) - item = element.find(xml_name) - if item is not None: - existed_attr_keys.append(xml_name) - if deser: - result[rest_name] = deser(item) - else: - result[rest_name] = _deserialize(rf_type, item) - elif kind == 1: # attribute - attr_val = element.get(xml_name) - if attr_val is not None: - existed_attr_keys.append(xml_name) - if deser: - result[rest_name] = deser(attr_val) - else: - result[rest_name] = attr_val - elif kind == 2: # unwrapped array - items = element.findall(items_name) # pyright: ignore - if len(items) > 0: - existed_attr_keys.append(items_name) - if deser: - result[rest_name] = deser(items) - else: - result[rest_name] = _deserialize(rf_type, items) - elif not is_optional: - existed_attr_keys.append(items_name) - result[rest_name] = [] - elif kind == 3: # text - if element.text is not None: - if deser: - result[rest_name] = deser(element.text) - else: - result[rest_name] = element.text - else: - model_meta = getattr(self, "_xml", {}) - for rf in self._attr_to_rest_field.values(): - prop_meta = getattr(rf, "_xml", {}) - xml_name = prop_meta.get("name", rf._rest_name) - xml_ns = _resolve_xml_ns(prop_meta, model_meta) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - # attribute - if prop_meta.get("attribute", False) and element.get(xml_name) is not None: - existed_attr_keys.append(xml_name) - result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name)) - continue - - # unwrapped element is array - if prop_meta.get("unwrapped", False): - _items_name = prop_meta.get("itemsName") - if _items_name: - xml_name = _items_name - _items_ns = prop_meta.get("itemsNs") - if _items_ns is not None: - xml_ns = _items_ns - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - items = element.findall(xml_name) # pyright: ignore - if len(items) > 0: - existed_attr_keys.append(xml_name) - result[rf._rest_name] = _deserialize(rf._type, items) - elif not rf._is_optional: - existed_attr_keys.append(xml_name) - result[rf._rest_name] = [] - continue - - # text element is primitive type - if prop_meta.get("text", False): - if element.text is not None: - result[rf._rest_name] = _deserialize(rf._type, element.text) - continue - - # wrapped element could be normal property or array - item = element.find(xml_name) - if item is not None: - existed_attr_keys.append(xml_name) - result[rf._rest_name] = _deserialize(rf._type, item) - - # rest thing is additional properties - for e in element: - if e.tag not in existed_attr_keys: - result[e.tag] = _convert_element(e) - - return result - - def copy(self) -> "Model": - return Model(self.__dict__) - - def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: - if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated: - # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping', - # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object' - mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order - attr_to_rest_field: dict[str, _RestField] = { # map attribute name to rest_field property - k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type") - } - annotations = { - k: v - for mro_class in mros - if hasattr(mro_class, "__annotations__") - for k, v in mro_class.__annotations__.items() - } - for attr, rf in attr_to_rest_field.items(): - rf._module = cls.__module__ - if not rf._type: - rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None)) - if not rf._rest_name_input: - rf._rest_name_input = attr - cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items()) - # Build XML field plan for fast _init_from_xml (only for XML models) - if getattr(cls, "_xml", None): - cls._xml_field_plan = _build_xml_field_plan(cls, attr_to_rest_field) - cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") - - return super().__new__(cls) - - def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None: - for base in cls.__bases__: - if hasattr(base, "__mapping__"): - base.__mapping__[discriminator or cls.__name__] = cls # type: ignore - - @classmethod - def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: - for v in cls.__dict__.values(): - if isinstance(v, _RestField) and v._is_discriminator and v._rest_name not in exist_discriminators: - return v - return None - - @classmethod - def _deserialize(cls, data, exist_discriminators): - if not hasattr(cls, "__mapping__"): - return cls(data) - discriminator = cls._get_discriminator(exist_discriminators) - if discriminator is None: - return cls(data) - exist_discriminators.append(discriminator._rest_name) - if isinstance(data, ET.Element): - model_meta = getattr(cls, "_xml", {}) - prop_meta = getattr(discriminator, "_xml", {}) - xml_name = prop_meta.get("name", discriminator._rest_name) - xml_ns = _resolve_xml_ns(prop_meta, model_meta) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - if data.get(xml_name) is not None: - discriminator_value = data.get(xml_name) - else: - discriminator_value = data.find(xml_name).text # pyright: ignore - else: - discriminator_value = data.get(discriminator._rest_name) - mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore # pylint: disable=no-member - return mapped_cls._deserialize(data, exist_discriminators) - - def as_dict(self, *, exclude_readonly: bool = False) -> dict[str, typing.Any]: - """Return a dict that can be turned into json using json.dump. - - :keyword bool exclude_readonly: Whether to remove the readonly properties. - :returns: A dict JSON compatible object - :rtype: dict - """ - - result = {} - readonly_props = [] - if exclude_readonly: - readonly_props = [p._rest_name for p in self._attr_to_rest_field.values() if _is_readonly(p)] - for k, v in self.items(): - if exclude_readonly and k in readonly_props: # pyright: ignore - continue - is_multipart_file_input = False - try: - is_multipart_file_input = next( - rf for rf in self._attr_to_rest_field.values() if rf._rest_name == k - )._is_multipart_file_input - except StopIteration: - pass - result[k] = v if is_multipart_file_input else Model._as_dict_value(v, exclude_readonly=exclude_readonly) - return result - - @staticmethod - def _as_dict_value(v: typing.Any, exclude_readonly: bool = False) -> typing.Any: - if v is None or isinstance(v, _Null): - return None - if isinstance(v, (list, tuple, set)): - return type(v)(Model._as_dict_value(x, exclude_readonly=exclude_readonly) for x in v) - if isinstance(v, dict): - return {dk: Model._as_dict_value(dv, exclude_readonly=exclude_readonly) for dk, dv in v.items()} - return v.as_dict(exclude_readonly=exclude_readonly) if hasattr(v, "as_dict") else v - - -def _deserialize_model(model_deserializer: typing.Optional[typing.Callable], obj): - if _is_model(obj): - return obj - return _deserialize(model_deserializer, obj) - - -def _deserialize_with_optional(if_obj_deserializer: typing.Optional[typing.Callable], obj): - if obj is None: - return obj - return _deserialize_with_callable(if_obj_deserializer, obj) - - -def _deserialize_with_union(deserializers, obj): - for deserializer in deserializers: - try: - return _deserialize(deserializer, obj) - except DeserializationError: - pass - raise DeserializationError() - - -def _deserialize_dict( - value_deserializer: typing.Optional[typing.Callable], - module: typing.Optional[str], - obj: dict[typing.Any, typing.Any], -): - if obj is None: - return obj - if isinstance(obj, ET.Element): - obj = {child.tag: child for child in obj} - return {k: _deserialize(value_deserializer, v, module) for k, v in obj.items()} - - -def _deserialize_multiple_sequence( - entry_deserializers: list[typing.Optional[typing.Callable]], - module: typing.Optional[str], - obj, -): - if obj is None: - return obj - return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers)) - - -def _is_array_encoded_deserializer(deserializer: functools.partial) -> bool: - return ( - isinstance(deserializer, functools.partial) - and isinstance(deserializer.args[0], functools.partial) - and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable - ) - - -def _deserialize_sequence( - deserializer: typing.Optional[typing.Callable], - module: typing.Optional[str], - obj, -): - if obj is None: - return obj - if isinstance(obj, ET.Element): - obj = list(obj) - - # encoded string may be deserialized to sequence - if isinstance(obj, str) and isinstance(deserializer, functools.partial): - # for list[str] - if _is_array_encoded_deserializer(deserializer): - return deserializer(obj) - - # for list[Union[...]] - if isinstance(deserializer.args[0], list): - for sub_deserializer in deserializer.args[0]: - if _is_array_encoded_deserializer(sub_deserializer): - return sub_deserializer(obj) - - return type(obj)(_deserialize(deserializer, entry, module) for entry in obj) - - -def _sorted_annotations(types: list[typing.Any]) -> list[typing.Any]: - return sorted( - types, - key=lambda x: hasattr(x, "__name__") and x.__name__.lower() in ("str", "float", "int", "bool"), - ) - - -def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-statements, too-many-branches - annotation: typing.Any, - module: typing.Optional[str], - rf: typing.Optional["_RestField"] = None, -) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: - if not annotation: - return None - - # is it a type alias? - if isinstance(annotation, str): - if module is not None: - annotation = _get_type_alias_type(module, annotation) - - # is it a forward ref / in quotes? - if isinstance(annotation, (str, typing.ForwardRef)): - try: - model_name = annotation.__forward_arg__ # type: ignore - except AttributeError: - model_name = annotation - if module is not None: - annotation = _get_model(module, model_name) # type: ignore - - try: - if module and _is_model(annotation): - if rf: - rf._is_model = True - - return functools.partial(_deserialize_model, annotation) # pyright: ignore - except Exception: - pass - - # is it a literal? - try: - if annotation.__origin__ is typing.Literal: # pyright: ignore - return None - except AttributeError: - pass - - # is it optional? - try: - if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore - if rf: - rf._is_optional = True - if len(annotation.__args__) <= 2: # pyright: ignore - if_obj_deserializer = _get_deserialize_callable_from_annotation( - next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore - ) - - return functools.partial(_deserialize_with_optional, if_obj_deserializer) - # the type is Optional[Union[...]], we need to remove the None type from the Union - annotation_copy = copy.copy(annotation) - annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a is not _NONE_TYPE] # pyright: ignore - return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) - except AttributeError: - pass - - # is it union? - if getattr(annotation, "__origin__", None) is typing.Union: - # initial ordering is we make `string` the last deserialization option, because it is often them most generic - deserializers = [ - _get_deserialize_callable_from_annotation(arg, module, rf) - for arg in _sorted_annotations(annotation.__args__) # pyright: ignore - ] - - return functools.partial(_deserialize_with_union, deserializers) - - try: - annotation_name = ( - annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore - ) - if annotation_name.lower() == "dict": - value_deserializer = _get_deserialize_callable_from_annotation( - annotation.__args__[1], module, rf # pyright: ignore - ) - - return functools.partial( - _deserialize_dict, - value_deserializer, - module, - ) - except (AttributeError, IndexError): - pass - try: - annotation_name = ( - annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore - ) - if annotation_name.lower() in ["list", "set", "tuple", "sequence"]: - if len(annotation.__args__) > 1: # pyright: ignore - entry_deserializers = [ - _get_deserialize_callable_from_annotation(dt, module, rf) - for dt in annotation.__args__ # pyright: ignore - ] - return functools.partial(_deserialize_multiple_sequence, entry_deserializers, module) - deserializer = _get_deserialize_callable_from_annotation( - annotation.__args__[0], module, rf # pyright: ignore - ) - - return functools.partial(_deserialize_sequence, deserializer, module) - except (TypeError, IndexError, AttributeError, SyntaxError): - pass - - def _deserialize_default( - deserializer, - obj, - ): - if obj is None: - return obj - try: - return _deserialize_with_callable(deserializer, obj) - except Exception: - pass - return obj - - if get_deserializer(annotation, rf): - return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) - - return functools.partial(_deserialize_default, annotation) - - -def _deserialize_with_callable( - deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]], - value: typing.Any, -): # pylint: disable=too-many-return-statements - try: - if value is None or isinstance(value, _Null): - return None - if isinstance(value, ET.Element): - if deserializer is str: - return value.text or "" - if deserializer is int: - return int(value.text) if value.text else None - if deserializer is float: - return float(value.text) if value.text else None - if deserializer is bool: - return value.text == "true" if value.text else None - if deserializer and deserializer in _DESERIALIZE_MAPPING.values(): - return deserializer(value.text) if value.text else None - if deserializer and deserializer in _DESERIALIZE_MAPPING_WITHFORMAT.values(): - return deserializer(value.text) if value.text else None - if deserializer is None: - return value - if deserializer in [int, float, bool]: - return deserializer(value) - if isinstance(deserializer, CaseInsensitiveEnumMeta): - try: - return deserializer(value.text if isinstance(value, ET.Element) else value) - except ValueError: - # for unknown value, return raw value - return value.text if isinstance(value, ET.Element) else value - if isinstance(deserializer, type) and issubclass(deserializer, Model): - return deserializer._deserialize(value, []) - return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value) - except Exception as e: - raise DeserializationError() from e - - -def _deserialize( - deserializer: typing.Any, - value: typing.Any, - module: typing.Optional[str] = None, - rf: typing.Optional["_RestField"] = None, - format: typing.Optional[str] = None, -) -> typing.Any: - if isinstance(value, PipelineResponse): - value = value.http_response.json() - if rf is None and format: - rf = _RestField(format=format) - if not isinstance(deserializer, functools.partial): - deserializer = _get_deserialize_callable_from_annotation(deserializer, module, rf) - return _deserialize_with_callable(deserializer, value) - - -def _failsafe_deserialize( - deserializer: typing.Any, - response: HttpResponse, - module: typing.Optional[str] = None, - rf: typing.Optional["_RestField"] = None, - format: typing.Optional[str] = None, -) -> typing.Any: - try: - return _deserialize(deserializer, response.json(), module, rf, format) - except Exception: # pylint: disable=broad-except - _LOGGER.warning( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - -def _failsafe_deserialize_xml( - deserializer: typing.Any, - response: HttpResponse, -) -> typing.Any: - try: - return _deserialize_xml(deserializer, response.text()) - except Exception: # pylint: disable=broad-except - _LOGGER.warning( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - -# pylint: disable=too-many-instance-attributes -class _RestField: - def __init__( - self, - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - is_discriminator: bool = False, - visibility: typing.Optional[list[str]] = None, - default: typing.Any = _UNSET, - format: typing.Optional[str] = None, - is_multipart_file_input: bool = False, - xml: typing.Optional[dict[str, typing.Any]] = None, - deserializer: typing.Optional[typing.Callable] = None, - ): - self._type = type - self._rest_name_input = name - self._module: typing.Optional[str] = None - self._is_discriminator = is_discriminator - self._visibility = visibility - self._is_model = False - self._is_optional = False - self._default = default - self._format = format - self._is_multipart_file_input = is_multipart_file_input - self._xml = xml if xml is not None else {} - self._deserializer = deserializer - - @property - def _class_type(self) -> typing.Any: - result = getattr(self._type, "args", [None])[0] - # type may be wrapped by nested functools.partial so we need to check for that - if isinstance(result, functools.partial): - return getattr(result, "args", [None])[0] - return result - - @property - def _rest_name(self) -> str: - if self._rest_name_input is None: - raise ValueError("Rest name was never set") - return self._rest_name_input - - def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin - # by this point, type and rest_name will have a value bc we default - # them in __new__ of the Model class - # Use _data.get() directly to avoid triggering __getitem__ which clears the cache - item = obj._data.get(self._rest_name, _UNSET) - if item is _UNSET: - # Field not set by user; return the client default if one exists, otherwise None - return self._default if self._default is not _UNSET else None - if item is None: - return item - if self._is_model: - return item - - # For mutable types, we want mutations to directly affect _data - # Check if we've already deserialized this value - cache_attr = f"_deserialized_{self._rest_name}" - if hasattr(obj, cache_attr): - # Return the value from _data directly (it's been deserialized in place) - return obj._data.get(self._rest_name) - - # Fast path: use _deserializer directly (avoids _serialize/_deserialize chain) - if self._deserializer: - deserialized = self._deserializer(item) - else: - deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self) - - # For mutable types, store the deserialized value back in _data - # so mutations directly affect _data - if isinstance(deserialized, (dict, list, set)): - obj._data[self._rest_name] = deserialized - object.__setattr__(obj, cache_attr, True) # Mark as deserialized - return deserialized - - return deserialized - - def __set__(self, obj: Model, value) -> None: - # Clear the cached deserialized object when setting a new value - cache_attr = f"_deserialized_{self._rest_name}" - if hasattr(obj, cache_attr): - object.__delattr__(obj, cache_attr) - - if value is None: - # we want to wipe out entries if users set attr to None - try: - obj.__delitem__(self._rest_name) - except KeyError: - pass - return - if self._is_model: - if not _is_model(value): - value = _deserialize(self._type, value) - obj.__setitem__(self._rest_name, value) - return - obj.__setitem__(self._rest_name, _serialize(value, self._format)) - - def _get_deserialize_callable_from_annotation( - self, annotation: typing.Any - ) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: - return _get_deserialize_callable_from_annotation(annotation, self._module, self) - - -def rest_field( - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[list[str]] = None, - default: typing.Any = _UNSET, - format: typing.Optional[str] = None, - is_multipart_file_input: bool = False, - xml: typing.Optional[dict[str, typing.Any]] = None, - deserializer: typing.Optional[typing.Callable] = None, -) -> typing.Any: - return _RestField( - name=name, - type=type, - visibility=visibility, - default=default, - format=format, - is_multipart_file_input=is_multipart_file_input, - xml=xml, - deserializer=deserializer, - ) - - -def rest_discriminator( - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[list[str]] = None, - xml: typing.Optional[dict[str, typing.Any]] = None, -) -> typing.Any: - return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml) - - -def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: - """Serialize a model to XML. - - :param Model model: The model to serialize. - :param bool exclude_readonly: Whether to exclude readonly properties. - :returns: The XML representation of the model. - :rtype: str - """ - return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore - - -def _get_xml_ns(meta: dict[str, typing.Any]) -> typing.Optional[str]: - """Return the XML namespace from a metadata dict, checking both 'ns' (old-style) and 'namespace' (DPG) keys. - - :param dict meta: The metadata dictionary to extract namespace from. - :returns: The namespace string if 'ns' or 'namespace' key is present, None otherwise. - :rtype: str or None - """ - ns = meta.get("ns") - if ns is None: - ns = meta.get("namespace") - return ns - - -def _resolve_xml_ns( - prop_meta: dict[str, typing.Any], model_meta: typing.Optional[dict[str, typing.Any]] = None -) -> typing.Optional[str]: - """Resolve XML namespace for a property, falling back to model namespace when appropriate. - - Checks the property metadata first; if no namespace is found and the model does not declare - an explicit prefix, falls back to the model-level namespace. - - :param dict prop_meta: The property metadata dictionary. - :param dict model_meta: The model metadata dictionary, used as fallback. - :returns: The resolved namespace string, or None. - :rtype: str or None - """ - ns = _get_xml_ns(prop_meta) - if ns is None and model_meta is not None and not model_meta.get("prefix"): - ns = _get_xml_ns(model_meta) - return ns - - -def _set_xml_attribute(element: ET.Element, name: str, value: typing.Any, prop_meta: dict[str, typing.Any]) -> None: - """Set an XML attribute on an element, handling namespace prefix registration. - - :param ET.Element element: The element to set the attribute on. - :param str name: The default attribute name (wire name). - :param any value: The attribute value. - :param dict prop_meta: The property metadata dictionary. - """ - xml_name = prop_meta.get("name", name) - _attr_ns = _get_xml_ns(prop_meta) - if _attr_ns: - _attr_prefix = prop_meta.get("prefix") - if _attr_prefix: - _safe_register_namespace(_attr_prefix, _attr_ns) - xml_name = "{" + _attr_ns + "}" + xml_name - element.set(xml_name, _get_primitive_type_value(value)) - - -def _get_element( - o: typing.Any, - exclude_readonly: bool = False, - parent_meta: typing.Optional[dict[str, typing.Any]] = None, - wrapped_element: typing.Optional[ET.Element] = None, -) -> typing.Union[ET.Element, list[ET.Element]]: - if _is_model(o): - model_meta = getattr(o, "_xml", {}) - - # if prop is a model, then use the prop element directly, else generate a wrapper of model - if wrapped_element is None: - # When serializing as an array item (parent_meta is set), check if the parent has an - # explicit itemsName. This ensures correct element names for unwrapped arrays (where - # the element tag is the property/items name, not the model type name). - _items_name = parent_meta.get("itemsName") if parent_meta is not None else None - element_name = _items_name if _items_name else (model_meta.get("name") or o.__class__.__name__) - _model_ns = _get_xml_ns(model_meta) - wrapped_element = _create_xml_element( - element_name, - model_meta.get("prefix"), - _model_ns, - ) - - readonly_props = [] - if exclude_readonly: - readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] - - for k, v in o.items(): - # do not serialize readonly properties - if exclude_readonly and k in readonly_props: - continue - - prop_rest_field = _get_rest_field(o._attr_to_rest_field, k) - if prop_rest_field: - prop_meta = getattr(prop_rest_field, "_xml").copy() - # use the wire name as xml name if no specific name is set - if prop_meta.get("name") is None: - prop_meta["name"] = k - else: - # additional properties will not have rest field, use the wire name as xml name - prop_meta = {"name": k} - - # Propagate model namespace to properties only for old-style "ns"-keyed models. - # DPG-generated models use the "namespace" key and explicitly declare namespace on - # each property that needs it, so propagation is intentionally skipped for them. - if prop_meta.get("ns") is None and model_meta.get("ns"): - prop_meta["ns"] = model_meta.get("ns") - prop_meta["prefix"] = model_meta.get("prefix") - - if prop_meta.get("unwrapped", False): - # unwrapped could only set on array - wrapped_element.extend(_get_element(v, exclude_readonly, prop_meta)) - elif prop_meta.get("text", False): - # text could only set on primitive type - wrapped_element.text = _get_primitive_type_value(v) - elif prop_meta.get("attribute", False): - _set_xml_attribute(wrapped_element, k, v, prop_meta) - else: - # other wrapped prop element - wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) - return wrapped_element - if isinstance(o, list): - return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore - if isinstance(o, dict): - result = [] - _dict_ns = _get_xml_ns(parent_meta) if parent_meta else None - for k, v in o.items(): - result.append( - _get_wrapped_element( - v, - exclude_readonly, - { - "name": k, - "ns": _dict_ns, - "prefix": parent_meta.get("prefix") if parent_meta else None, - }, - ) - ) - return result - - # primitive case need to create element based on parent_meta - if parent_meta: - _items_ns = parent_meta.get("itemsNs") - if _items_ns is None: - _items_ns = _get_xml_ns(parent_meta) - return _get_wrapped_element( - o, - exclude_readonly, - { - "name": parent_meta.get("itemsName", parent_meta.get("name")), - "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")), - "ns": _items_ns, - }, - ) - - raise ValueError("Could not serialize value into xml: " + o) - - -def _get_wrapped_element( - v: typing.Any, - exclude_readonly: bool, - meta: typing.Optional[dict[str, typing.Any]], -) -> ET.Element: - _meta_ns = _get_xml_ns(meta) if meta else None - wrapped_element = _create_xml_element( - meta.get("name") if meta else None, meta.get("prefix") if meta else None, _meta_ns - ) - if isinstance(v, (dict, list)): - wrapped_element.extend(_get_element(v, exclude_readonly, meta)) - elif _is_model(v): - _get_element(v, exclude_readonly, meta, wrapped_element) - else: - wrapped_element.text = _get_primitive_type_value(v) - return wrapped_element # type: ignore[no-any-return] - - -def _get_primitive_type_value(v) -> str: - if v is True: - return "true" - if v is False: - return "false" - if isinstance(v, _Null): - return "" - return str(v) - - -def _safe_register_namespace(prefix: str, ns: str) -> None: - """Register an XML namespace prefix, handling reserved prefix patterns. - - Some prefixes (e.g. 'ns2') match Python's reserved 'ns\\d+' pattern used for - auto-generated prefixes, causing register_namespace to raise ValueError. - Falls back to directly registering in the internal namespace map. - - :param str prefix: The namespace prefix to register. - :param str ns: The namespace URI. - """ - try: - ET.register_namespace(prefix, ns) - except ValueError: - _ns_map = getattr(ET, "_namespace_map", None) - if _ns_map is not None: - _ns_map[ns] = prefix - - -def _create_xml_element( - tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None -) -> ET.Element: - if prefix and ns: - _safe_register_namespace(prefix, ns) - if ns: - return ET.Element("{" + ns + "}" + tag) - return ET.Element(tag) - - -def _deserialize_xml( - deserializer: typing.Any, - value: str, -) -> typing.Any: - element = ET.fromstring(value) # nosec - if _is_model(deserializer): - return deserializer._deserialize(element, []) - return _deserialize(deserializer, element) - - -def _convert_element(e: ET.Element): - # dict case - if len(e.attrib) > 0 or len({child.tag for child in e}) > 1: - dict_result: dict[str, typing.Any] = {} - for child in e: - if dict_result.get(child.tag) is not None: - if isinstance(dict_result[child.tag], list): - dict_result[child.tag].append(_convert_element(child)) - else: - dict_result[child.tag] = [dict_result[child.tag], _convert_element(child)] - else: - dict_result[child.tag] = _convert_element(child) - dict_result.update(e.attrib) - return dict_result - # array case - if len(e) > 0: - array_result: list[typing.Any] = [] - for child in e: - array_result.append(_convert_element(child)) - return array_result - # primitive case - return e.text diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py deleted file mode 100644 index 4e2942f574c4..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/_version.py +++ /dev/null @@ -1,9 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - -VERSION = "1.6.0" diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py index 1ddad049d3ea..7d62ac6effd1 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/__init__.py @@ -2,28 +2,22 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: disable=wrong-import-position -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from ._patch import * # pylint: disable=unused-wildcard-import - -from ._client import IdentityClient # type: ignore +from ._client import CommunicationIdentityClient try: from ._patch import __all__ as _patch_all - from ._patch import * + from ._patch import * # pylint: disable=unused-wildcard-import except ImportError: _patch_all = [] from ._patch import patch_sdk as _patch_sdk __all__ = [ - "IdentityClient", + "CommunicationIdentityClient", ] -__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore +__all__.extend([p for p in _patch_all if p not in __all__]) _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py index 2aaabe8b1832..0c09e59bf350 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_client.py @@ -2,47 +2,33 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from copy import deepcopy -import sys from typing import Any, Awaitable from azure.core import AsyncPipelineClient -from azure.core.pipeline import policies from azure.core.rest import AsyncHttpResponse, HttpRequest -from .._utils.serialization import Deserializer, Serializer -from ._configuration import IdentityClientConfiguration -from .operations import IdentityOperationsOperations, TeamsExtensionOperationsOperations, TeamsUserOperationsOperations +from .. import models as _models +from .._serialization import Deserializer, Serializer +from ._configuration import CommunicationIdentityClientConfiguration +from .operations import CommunicationIdentityOperations -if sys.version_info >= (3, 11): - from typing import Self -else: - from typing_extensions import Self # type: ignore - -class IdentityClient: # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationIdentityClient: # pylint: disable=client-accepts-api-version-keyword """Azure Communication Identity Service. - :ivar identity_operations: IdentityOperationsOperations operations - :vartype identity_operations: - azure.communication.identity.aio.operations.IdentityOperationsOperations - :ivar teams_user_operations: TeamsUserOperationsOperations operations - :vartype teams_user_operations: - azure.communication.identity.aio.operations.TeamsUserOperationsOperations - :ivar teams_extension_operations: TeamsExtensionOperationsOperations operations - :vartype teams_extension_operations: - azure.communication.identity.aio.operations.TeamsExtensionOperationsOperations + :ivar communication_identity: CommunicationIdentityOperations operations + :vartype communication_identity: + azure.communication.identity.aio.operations.CommunicationIdentityOperations :param endpoint: The communication resource, for example - `https://my-resource.communication.azure.com `_. - Required. + https://my-resource.communication.azure.com. Required. :type endpoint: str - :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this + default value may result in unsupported behavior. :paramtype api_version: str """ @@ -50,43 +36,18 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self, endpoint: str, **kwargs: Any ) -> None: _endpoint = "{endpoint}" - self._config = IdentityClientConfiguration(endpoint=endpoint, **kwargs) - - _policies = kwargs.pop("policies", None) - if _policies is None: - _policies = [ - policies.RequestIdPolicy(**kwargs), - self._config.headers_policy, - self._config.user_agent_policy, - self._config.proxy_policy, - policies.ContentDecodePolicy(**kwargs), - self._config.redirect_policy, - self._config.retry_policy, - self._config.authentication_policy, - self._config.custom_hook_policy, - self._config.logging_policy, - policies.DistributedTracingPolicy(**kwargs), - policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None, - self._config.http_logging_policy, - ] - self._client: AsyncPipelineClient = AsyncPipelineClient(base_url=_endpoint, policies=_policies, **kwargs) - - self._serialize = Serializer() - self._deserialize = Deserializer() + self._config = CommunicationIdentityClientConfiguration(endpoint=endpoint, **kwargs) + self._client: AsyncPipelineClient = AsyncPipelineClient(base_url=_endpoint, config=self._config, **kwargs) + + client_models = {k: v for k, v in _models.__dict__.items() if isinstance(v, type)} + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) self._serialize.client_side_validation = False - self.identity_operations = IdentityOperationsOperations( - self._client, self._config, self._serialize, self._deserialize - ) - self.teams_user_operations = TeamsUserOperationsOperations( - self._client, self._config, self._serialize, self._deserialize - ) - self.teams_extension_operations = TeamsExtensionOperationsOperations( + self.communication_identity = CommunicationIdentityOperations( self._client, self._config, self._serialize, self._deserialize ) - def send_request( - self, request: HttpRequest, *, stream: bool = False, **kwargs: Any - ) -> Awaitable[AsyncHttpResponse]: + def send_request(self, request: HttpRequest, **kwargs: Any) -> Awaitable[AsyncHttpResponse]: """Runs the network request through the client's chained policies. >>> from azure.core.rest import HttpRequest @@ -110,12 +71,12 @@ def send_request( } request_copy.url = self._client.format_url(request_copy.url, **path_format_arguments) - return self._client.send_request(request_copy, stream=stream, **kwargs) # type: ignore + return self._client.send_request(request_copy, **kwargs) async def close(self) -> None: await self._client.close() - async def __aenter__(self) -> Self: + async def __aenter__(self) -> "CommunicationIdentityClient": await self._client.__aenter__() return self diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py index d67c45d46467..fe759886cbce 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_configuration.py @@ -1,45 +1,45 @@ -# pylint: disable=line-too-long,useless-suppression # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from typing import Any +from azure.core.configuration import Configuration from azure.core.pipeline import policies -from .._version import VERSION +VERSION = "unknown" -class IdentityClientConfiguration: # pylint: disable=too-many-instance-attributes,docstring-keyword-should-match-keyword-only - """Configuration for IdentityClient. +class CommunicationIdentityClientConfiguration( # pylint: disable=too-many-instance-attributes,name-too-long + Configuration +): + """Configuration for CommunicationIdentityClient. Note that all parameters used to create this instance are saved as instance attributes. :param endpoint: The communication resource, for example - `https://my-resource.communication.azure.com `_. - Required. + https://my-resource.communication.azure.com. Required. :type endpoint: str - :keyword api_version: The API version to use for this operation. Known values are "2026-09-23" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: Api Version. Default value is "2023-10-01". Note that overriding this + default value may result in unsupported behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2026-09-23") + super(CommunicationIdentityClientConfiguration, self).__init__(**kwargs) + api_version: str = kwargs.pop("api_version", "2023-10-01") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") self.endpoint = endpoint self.api_version = api_version - kwargs.setdefault("sdk_moniker", "communication-identity/{}".format(VERSION)) - self.polling_interval = kwargs.get("polling_interval", 30) + kwargs.setdefault("sdk_moniker", "communicationidentityclient/{}".format(VERSION)) self._configure(**kwargs) def _configure(self, **kwargs: Any) -> None: @@ -48,7 +48,7 @@ def _configure(self, **kwargs: Any) -> None: self.proxy_policy = kwargs.get("proxy_policy") or policies.ProxyPolicy(**kwargs) self.logging_policy = kwargs.get("logging_policy") or policies.NetworkTraceLoggingPolicy(**kwargs) self.http_logging_policy = kwargs.get("http_logging_policy") or policies.HttpLoggingPolicy(**kwargs) + self.retry_policy = kwargs.get("retry_policy") or policies.AsyncRetryPolicy(**kwargs) self.custom_hook_policy = kwargs.get("custom_hook_policy") or policies.CustomHookPolicy(**kwargs) self.redirect_policy = kwargs.get("redirect_policy") or policies.AsyncRedirectPolicy(**kwargs) - self.retry_policy = kwargs.get("retry_policy") or policies.AsyncRetryPolicy(**kwargs) self.authentication_policy = kwargs.get("authentication_policy") diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py index ea765788358a..17dbc073e01b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/_patch.py @@ -1,20 +1,32 @@ # coding=utf-8 # -------------------------------------------------------------------------- +# # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. +# +# The MIT License (MIT) +# +# 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. +# # -------------------------------------------------------------------------- -"""Customize generated code here. - -Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize -""" - -__all__: list[str] = [] # Add all objects you want publicly available to users at this package level +# This file is used for handwritten extensions to the generated code. Example: +# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/customize_code/how-to-patch-sdk-code.md def patch_sdk(): - """Do not remove from this file. - - `patch_sdk` is a last resort escape hatch that allows you to do customizations - you can't accomplish using the techniques described in - https://aka.ms/azsdk/python/dpcodegen/python/customize - """ + pass diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py index 46690414aedf..2c8ddc0b3dd9 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/__init__.py @@ -2,28 +2,18 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: disable=wrong-import-position -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from ._patch import * # pylint: disable=unused-wildcard-import - -from ._operations import IdentityOperationsOperations # type: ignore -from ._operations import TeamsUserOperationsOperations # type: ignore -from ._operations import TeamsExtensionOperationsOperations # type: ignore +from ._operations import CommunicationIdentityOperations from ._patch import __all__ as _patch_all -from ._patch import * +from ._patch import * # pylint: disable=unused-wildcard-import from ._patch import patch_sdk as _patch_sdk __all__ = [ - "IdentityOperationsOperations", - "TeamsUserOperationsOperations", - "TeamsExtensionOperationsOperations", + "CommunicationIdentityOperations", ] -__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore +__all__.extend([p for p in _patch_all if p not in __all__]) _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py index 1db7bd9ffd84..d3449860771f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_operations.py @@ -3,23 +3,18 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from collections.abc import MutableMapping from io import IOBase -import json -from typing import Any, Callable, IO, Optional, TypeVar, Union, overload +from typing import Any, Callable, Dict, IO, Optional, TypeVar, Union, overload -from azure.core import AsyncPipelineClient from azure.core.exceptions import ( ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, ResourceNotModifiedError, - StreamClosedError, - StreamConsumedError, map_error, ) from azure.core.pipeline import PipelineResponse @@ -27,42 +22,37 @@ from azure.core.tracing.decorator_async import distributed_trace_async from azure.core.utils import case_insensitive_dict -from ... import models as _models, types as _types -from ..._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize -from ..._utils.serialization import Deserializer, Serializer +from ... import models as _models from ...operations._operations import ( - build_identity_operations_create_request, - build_identity_operations_delete_request, - build_identity_operations_issue_access_token_request, - build_identity_operations_revoke_access_tokens_request, - build_teams_extension_operations_delete_assignment_request, - build_teams_extension_operations_exchange_token_request, - build_teams_extension_operations_get_assignment_request, - build_teams_extension_operations_upsert_assignment_request, - build_teams_user_operations_exchange_teams_user_access_token_request, + build_communication_identity_create_request, + build_communication_identity_delete_request, + build_communication_identity_exchange_teams_user_access_token_request, + build_communication_identity_issue_access_token_request, + build_communication_identity_revoke_access_tokens_request, ) -from .._configuration import IdentityClientConfiguration T = TypeVar("T") -ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, dict[str, Any]], Any]] +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] -class IdentityOperationsOperations: # pylint: disable=docstring-missing-param +class CommunicationIdentityOperations: """ .. warning:: **DO NOT** instantiate this class directly. Instead, you should access the following operations through - :class:`~azure.communication.identity.aio.IdentityClient`'s - :attr:`identity_operations` attribute. + :class:`~azure.communication.identity.aio.CommunicationIdentityClient`'s + :attr:`communication_identity` attribute. """ + models = _models + def __init__(self, *args, **kwargs) -> None: input_args = list(args) - self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") - self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload async def create( @@ -78,23 +68,18 @@ async def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest + :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @overload async def create( - self, - body: Optional[_types.CommunicationIdentityCreateRequest] = None, - *, - content_type: str = "application/json", - **kwargs: Any + self, body: Optional[IO] = None, *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessTokenResult: """Create a new identity, and optionally, an access token. @@ -102,43 +87,18 @@ async def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - async def create( - self, body: Optional[IO[bytes]] = None, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Create a new identity, and optionally, an access token. - - Create a new identity, and optionally, an access token. - - :param body: If specified, creates also a Communication Identity access token associated with - the identity and containing the requested scopes. Default value is None. - :type body: IO[bytes] + :type body: IO :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace_async async def create( - self, - body: Optional[ - Union[_models.CommunicationIdentityCreateRequest, _types.CommunicationIdentityCreateRequest, IO[bytes]] - ] = None, - **kwargs: Any + self, body: Optional[Union[_models.CommunicationIdentityCreateRequest, IO]] = None, **kwargs: Any ) -> _models.CommunicationIdentityAccessTokenResult: """Create a new identity, and optionally, an access token. @@ -146,16 +106,16 @@ async def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Is either a - CommunicationIdentityCreateRequest type or a IO[bytes] type. Default value is None. - :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest - or ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest or - IO[bytes] - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + CommunicationIdentityCreateRequest type or a IO type. Default value is None. + :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest or IO + :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. + Default value is None. + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -167,22 +127,23 @@ async def create( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - content_type = content_type if body else None cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) - content_type = content_type or "application/json" if body else None + content_type = content_type or "application/json" + _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: if body is not None: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + _json = self._serialize.body(body, "CommunicationIdentityCreateRequest") else: - _content = None + _json = None - _request = build_identity_operations_create_request( + request = build_communication_identity_create_request( content_type=content_type, api_version=self._config.api_version, + json=_json, content=_content, headers=_headers, params=_params, @@ -190,41 +151,31 @@ async def create( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [201]: if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + await response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) + deserialized = self._deserialize("CommunicationIdentityAccessTokenResult", pipeline_response) if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore + return cls(pipeline_response, deserialized, {}) - return deserialized # type: ignore + return deserialized @distributed_trace_async - async def delete(self, id: str, **kwargs: Any) -> None: + async def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements """Delete the identity, revoke all tokens for the identity and delete all associated data. Delete the identity, revoke all tokens for the identity and delete all associated data. @@ -235,7 +186,7 @@ async def delete(self, id: str, **kwargs: Any) -> None: :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -248,7 +199,7 @@ async def delete(self, id: str, **kwargs: Any) -> None: cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_identity_operations_delete_request( + request = build_communication_identity_delete_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -257,28 +208,29 @@ async def delete(self, id: str, **kwargs: Any) -> None: path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: + if _stream: + await response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) if cls: - return cls(pipeline_response, None, {}) # type: ignore + return cls(pipeline_response, None, {}) @distributed_trace_async - async def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: + async def revoke_access_tokens( # pylint: disable=inconsistent-return-statements + self, id: str, **kwargs: Any + ) -> None: """Revoke all access tokens for the specific identity. Revoke all access tokens for the specific identity. @@ -289,7 +241,7 @@ async def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -302,7 +254,7 @@ async def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_identity_operations_revoke_access_tokens_request( + request = build_communication_identity_revoke_access_tokens_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -311,295 +263,86 @@ async def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if cls: - return cls(pipeline_response, None, {}) # type: ignore - - @overload - async def issue_access_token( - self, - id: str, - body: _models.CommunicationIdentityAccessTokenRequest, - *, - content_type: str = "application/json", - **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: - ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - async def issue_access_token( - self, - id: str, - body: _types.CommunicationIdentityAccessTokenRequest, - *, - content_type: str = "application/json", - **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: - ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - async def issue_access_token( - self, id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: IO[bytes] - :keyword content_type: Body Parameter content-type. Content type parameter for binary body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @distributed_trace_async - async def issue_access_token( - self, - id: str, - body: Union[ - _models.CommunicationIdentityAccessTokenRequest, _types.CommunicationIdentityAccessTokenRequest, IO[bytes] - ], - **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Is either a - CommunicationIdentityAccessTokenRequest type or a IO[bytes] type. Required. - :type body: - ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest or - ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest or - IO[bytes] - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = kwargs.pop("params", {}) or {} - - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) - - content_type = content_type or "application/json" - _content = None - if isinstance(body, (IOBase, bytes)): - _content = body - else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - - _request = build_identity_operations_issue_access_token_request( - id=id, - content_type=content_type, - api_version=self._config.api_version, - content=_content, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + await response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) - if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - -class TeamsUserOperationsOperations: # pylint: disable=docstring-missing-param - """ - .. warning:: - **DO NOT** instantiate this class directly. - - Instead, you should access the following operations through - :class:`~azure.communication.identity.aio.IdentityClient`'s - :attr:`teams_user_operations` attribute. - """ - - def __init__(self, *args, **kwargs) -> None: - input_args = list(args) - self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") - self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") + return cls(pipeline_response, None, {}) @overload async def exchange_teams_user_access_token( self, body: _models.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. - - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. - - :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - async def exchange_teams_user_access_token( - self, body: _types.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest + :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @overload async def exchange_teams_user_access_token( - self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + self, body: IO, *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: IO[bytes] + :type body: IO :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace_async async def exchange_teams_user_access_token( - self, - body: Union[_models.TeamsUserExchangeTokenRequest, _types.TeamsUserExchangeTokenRequest, IO[bytes]], - **kwargs: Any + self, body: Union[_models.TeamsUserExchangeTokenRequest, IO], **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. :param body: Request payload for the token exchange. Is either a TeamsUserExchangeTokenRequest - type or a IO[bytes] type. Required. - :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest or - ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest or IO[bytes] - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + type or a IO type. Required. + :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest or IO + :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. + Default value is None. + :paramtype content_type: str + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -614,15 +357,17 @@ async def exchange_teams_user_access_token( cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) content_type = content_type or "application/json" + _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + _json = self._serialize.body(body, "TeamsUserExchangeTokenRequest") - _request = build_teams_user_operations_exchange_teams_user_access_token_request( + request = build_communication_identity_exchange_teams_user_access_token_request( content_type=content_type, api_version=self._config.api_version, + json=_json, content=_content, headers=_headers, params=_params, @@ -630,386 +375,95 @@ async def exchange_teams_user_access_token( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [200]: if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + await response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) + deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - -class TeamsExtensionOperationsOperations: # pylint: disable=docstring-missing-param - """ - .. warning:: - **DO NOT** instantiate this class directly. - - Instead, you should access the following operations through - :class:`~azure.communication.identity.aio.IdentityClient`'s - :attr:`teams_extension_operations` attribute. - """ + return cls(pipeline_response, deserialized, {}) - def __init__(self, *args, **kwargs) -> None: - input_args = list(args) - self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") - self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") + return deserialized @overload - async def exchange_token( - self, body: _models.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. - - Exchanges a Teams Phone token for an ACS user access token. - - :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - async def exchange_token( - self, body: _types.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. + async def issue_access_token( + self, + id: str, + body: _models.CommunicationIdentityAccessTokenRequest, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. - Exchanges a Teams Phone token for an ACS user access token. + Issue a new token for an identity. - :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @overload - async def exchange_token( - self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. + async def issue_access_token( + self, id: str, body: IO, *, content_type: str = "application/json", **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. - Exchanges a Teams Phone token for an ACS user access token. + Issue a new token for an identity. - :param body: Request payload for the token exchange. Required. - :type body: IO[bytes] + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: IO :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @distributed_trace_async - async def exchange_token( - self, - body: Union[_models.TeamsExtensionExchangeTokenRequest, _types.TeamsExtensionExchangeTokenRequest, IO[bytes]], - **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. - - Exchanges a Teams Phone token for an ACS user access token. - - :param body: Request payload for the token exchange. Is either a - TeamsExtensionExchangeTokenRequest type or a IO[bytes] type. Required. - :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest - or ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest or - IO[bytes] - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = kwargs.pop("params", {}) or {} - - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) - - content_type = content_type or "application/json" - _content = None - if isinstance(body, (IOBase, bytes)): - _content = body - else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - - _request = build_teams_extension_operations_exchange_token_request( - content_type=content_type, - api_version=self._config.api_version, - content=_content, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: - if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) - - if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore @distributed_trace_async - async def get_assignment( - self, tenant_id: str, object_id: str, **kwargs: Any - ) -> _models.TeamsExtensionAssignmentResponse: - """Get Teams Phone access assignment by object id. - - Get Teams Phone access assignment by object id. - - :param tenant_id: Tenant Id we want to get the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to get the assignment for. Required. - :type object_id: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) - - _request = build_teams_extension_operations_get_assignment_request( - tenant_id=tenant_id, - object_id=object_id, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: - if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) - - if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - @overload - async def upsert_assignment( - self, - tenant_id: str, - object_id: str, - body: _models.TeamsExtensionAssignmentCreateOrUpdateRequest, - *, - content_type: str = "application/json", - **kwargs: Any - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Required. - :type body: - ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse - :raises ~azure.core.exceptions.HttpResponseError: - """ + async def issue_access_token( + self, id: str, body: Union[_models.CommunicationIdentityAccessTokenRequest, IO], **kwargs: Any + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. - @overload - async def upsert_assignment( - self, - tenant_id: str, - object_id: str, - body: _types.TeamsExtensionAssignmentCreateOrUpdateRequest, - *, - content_type: str = "application/json", - **kwargs: Any - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Required. - :type body: - ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse - :raises ~azure.core.exceptions.HttpResponseError: - """ + Issue a new token for an identity. - @overload - async def upsert_assignment( - self, tenant_id: str, object_id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Required. - :type body: IO[bytes] - :keyword content_type: Body Parameter content-type. Content type parameter for binary body. - Default value is "application/json". + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Is either a + CommunicationIdentityAccessTokenRequest type or a IO type. Required. + :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest or IO + :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. + Default value is None. :paramtype content_type: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @distributed_trace_async - async def upsert_assignment( - self, - tenant_id: str, - object_id: str, - body: Union[ - _models.TeamsExtensionAssignmentCreateOrUpdateRequest, - _types.TeamsExtensionAssignmentCreateOrUpdateRequest, - IO[bytes], - ], - **kwargs: Any - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Is either a TeamsExtensionAssignmentCreateOrUpdateRequest - type or a IO[bytes] type. Required. - :type body: - ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest - or ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest - or IO[bytes] - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -1021,20 +475,21 @@ async def upsert_assignment( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) + cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) content_type = content_type or "application/json" + _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + _json = self._serialize.body(body, "CommunicationIdentityAccessTokenRequest") - _request = build_teams_extension_operations_upsert_assignment_request( - tenant_id=tenant_id, - object_id=object_id, + request = build_communication_identity_issue_access_token_request( + id=id, content_type=content_type, api_version=self._config.api_version, + json=_json, content=_content, headers=_headers, params=_params, @@ -1042,92 +497,25 @@ async def upsert_assignment( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response - if response.status_code not in [200, 201]: + if response.status_code not in [200]: if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + await response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) + deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - @distributed_trace_async - async def delete_assignment(self, tenant_id: str, object_id: str, **kwargs: Any) -> None: - """Removes a Teams Phone access assignment. - - Removes a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to remove the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to remove the assignment for. Required. - :type object_id: str - :return: None - :rtype: None - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) + return cls(pipeline_response, deserialized, {}) - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[None] = kwargs.pop("cls", None) - - _request = build_teams_extension_operations_delete_assignment_request( - tenant_id=tenant_id, - object_id=object_id, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [204]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if cls: - return cls(pipeline_response, None, {}) # type: ignore + return deserialized diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py index ea765788358a..f7dd32510333 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/aio/operations/_patch.py @@ -1,14 +1,14 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------- +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ +from typing import List -__all__: list[str] = [] # Add all objects you want publicly available to users at this package level +__all__: List[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py index 5daaa68dd7e1..9a71cff104ed 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/__init__.py @@ -2,37 +2,22 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: disable=wrong-import-position -from typing import TYPE_CHECKING +from ._models import CommunicationError +from ._models import CommunicationErrorResponse +from ._models import CommunicationIdentity +from ._models import CommunicationIdentityAccessToken +from ._models import CommunicationIdentityAccessTokenRequest +from ._models import CommunicationIdentityAccessTokenResult +from ._models import CommunicationIdentityCreateRequest +from ._models import TeamsUserExchangeTokenRequest -if TYPE_CHECKING: - from ._patch import * # pylint: disable=unused-wildcard-import - - -from ._models import ( # type: ignore - CommunicationError, - CommunicationErrorResponse, - CommunicationIdentity, - CommunicationIdentityAccessToken, - CommunicationIdentityAccessTokenRequest, - CommunicationIdentityAccessTokenResult, - CommunicationIdentityCreateRequest, - TeamsExtensionAssignmentCreateOrUpdateRequest, - TeamsExtensionAssignmentResponse, - TeamsExtensionExchangeTokenRequest, - TeamsUserExchangeTokenRequest, -) - -from ._enums import ( # type: ignore - CommunicationTokenScope, - TeamsExtensionPrincipalType, -) +from ._enums import CommunicationTokenScope from ._patch import __all__ as _patch_all -from ._patch import * +from ._patch import * # pylint: disable=unused-wildcard-import from ._patch import patch_sdk as _patch_sdk __all__ = [ @@ -43,12 +28,8 @@ "CommunicationIdentityAccessTokenRequest", "CommunicationIdentityAccessTokenResult", "CommunicationIdentityCreateRequest", - "TeamsExtensionAssignmentCreateOrUpdateRequest", - "TeamsExtensionAssignmentResponse", - "TeamsExtensionExchangeTokenRequest", "TeamsUserExchangeTokenRequest", "CommunicationTokenScope", - "TeamsExtensionPrincipalType", ] -__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore +__all__.extend([p for p in _patch_all if p not in __all__]) _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py index 5618e7dff5f6..e95bddd653ed 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_enums.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- @@ -21,15 +21,6 @@ class CommunicationTokenScope(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Access to Chat APIs but without the authorization to create, delete or update chat threads.""" CHAT_JOIN_LIMITED = "chat.join.limited" """A more limited version of chat.join that doesn't allow to add or remove participants. Use this - scope when the token bearer is not fully trusted, for example in guest scenarios.""" + #: scope when the token bearer is not fully trusted, for example in guest scenarios.""" VOIP_JOIN = "voip.join" """Access to Calling APIs but without the authorization to start new calls.""" - - -class TeamsExtensionPrincipalType(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """The type of principal the assignment is for.""" - - RESOURCE_ACCOUNT = "resourceAccount" - """Resource account principal type.""" - USER = "user" - """User principal type.""" diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py index 3d954eddc038..a9f24485d60b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_models.py @@ -1,24 +1,28 @@ # coding=utf-8 +# pylint: disable=too-many-lines # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: disable=useless-super-delegation -import datetime -from typing import Any, Mapping, Optional, TYPE_CHECKING, Union, overload +from typing import Any, List, Optional, TYPE_CHECKING, Union -from .._utils.model_base import Model as _Model, rest_field +from .. import _serialization if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports from .. import models as _models -class CommunicationError(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationError(_serialization.Model): """The Communication Services error. + Variables are only populated by the server, and will be ignored when sending a request. + + All required parameters must be populated in order to send to Azure. + :ivar code: The error code. Required. :vartype code: str :ivar message: The error message. Required. @@ -26,134 +30,131 @@ class CommunicationError(_Model): # pylint: disable=docstring-keyword-should-ma :ivar target: The error target. :vartype target: str :ivar details: Further details about specific errors that led to this error. - :vartype details: list[~azure.communication.identity._generated.models.CommunicationError] + :vartype details: list[~azure.communication.identity.models.CommunicationError] :ivar inner_error: The inner error if any. - :vartype inner_error: ~azure.communication.identity._generated.models.CommunicationError + :vartype inner_error: ~azure.communication.identity.models.CommunicationError """ - code: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """The error code. Required.""" - message: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """The error message. Required.""" - target: Optional[str] = rest_field(visibility=["read"]) - """The error target.""" - details: Optional[list["_models.CommunicationError"]] = rest_field(visibility=["read"]) - """Further details about specific errors that led to this error.""" - inner_error: Optional["_models.CommunicationError"] = rest_field(name="innererror", visibility=["read"]) - """The inner error if any.""" - - @overload - def __init__( - self, - *, - code: str, - message: str, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + _validation = { + "code": {"required": True}, + "message": {"required": True}, + "target": {"readonly": True}, + "details": {"readonly": True}, + "inner_error": {"readonly": True}, + } + + _attribute_map = { + "code": {"key": "code", "type": "str"}, + "message": {"key": "message", "type": "str"}, + "target": {"key": "target", "type": "str"}, + "details": {"key": "details", "type": "[CommunicationError]"}, + "inner_error": {"key": "innererror", "type": "CommunicationError"}, + } + + def __init__(self, *, code: str, message: str, **kwargs: Any) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword code: The error code. Required. + :paramtype code: str + :keyword message: The error message. Required. + :paramtype message: str """ + super().__init__(**kwargs) + self.code = code + self.message = message + self.target = None + self.details = None + self.inner_error = None - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - -class CommunicationErrorResponse(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationErrorResponse(_serialization.Model): """The Communication Services error. + All required parameters must be populated in order to send to Azure. + :ivar error: The Communication Services error. Required. - :vartype error: ~azure.communication.identity._generated.models.CommunicationError + :vartype error: ~azure.communication.identity.models.CommunicationError """ - error: "_models.CommunicationError" = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """The Communication Services error. Required.""" + _validation = { + "error": {"required": True}, + } - @overload - def __init__( - self, - *, - error: "_models.CommunicationError", - ) -> None: ... + _attribute_map = { + "error": {"key": "error", "type": "CommunicationError"}, + } - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + def __init__(self, *, error: "_models.CommunicationError", **kwargs: Any) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword error: The Communication Services error. Required. + :paramtype error: ~azure.communication.identity.models.CommunicationError """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + super().__init__(**kwargs) + self.error = error -class CommunicationIdentity(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationIdentity(_serialization.Model): """A communication identity. + All required parameters must be populated in order to send to Azure. + :ivar id: Identifier of the identity. Required. :vartype id: str """ - id: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """Identifier of the identity. Required.""" + _validation = { + "id": {"required": True}, + } - @overload - def __init__( - self, - *, - id: str, # pylint: disable=redefined-builtin - ) -> None: ... + _attribute_map = { + "id": {"key": "id", "type": "str"}, + } - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + def __init__(self, *, id: str, **kwargs: Any) -> None: # pylint: disable=redefined-builtin """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword id: Identifier of the identity. Required. + :paramtype id: str """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + super().__init__(**kwargs) + self.id = id -class CommunicationIdentityAccessToken(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationIdentityAccessToken(_serialization.Model): """An access token. + All required parameters must be populated in order to send to Azure. + :ivar token: The access token issued for the identity. Required. :vartype token: str :ivar expires_on: The expiry time of the token. Required. - :vartype expires_on: ~datetime.datetime + :vartype expires_on: str """ - token: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """The access token issued for the identity. Required.""" - expires_on: datetime.datetime = rest_field( - name="expiresOn", visibility=["read", "create", "update", "delete", "query"], format="rfc3339" - ) - """The expiry time of the token. Required.""" + _validation = { + "token": {"required": True}, + "expires_on": {"required": True}, + } - @overload - def __init__( - self, - *, - token: str, - expires_on: datetime.datetime, - ) -> None: ... + _attribute_map = { + "token": {"key": "token", "type": "str"}, + "expires_on": {"key": "expiresOn", "type": "str"}, + } - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + def __init__(self, *, token: str, expires_on: str, **kwargs: Any) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword token: The access token issued for the identity. Required. + :paramtype token: str + :keyword expires_on: The expiry time of the token. Required. + :paramtype expires_on: str """ + super().__init__(**kwargs) + self.token = token + self.expires_on = expires_on - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) +class CommunicationIdentityAccessTokenRequest(_serialization.Model): + """CommunicationIdentityAccessTokenRequest. -class CommunicationIdentityAccessTokenRequest(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only - """Request to issue a new access token for an identity. + All required parameters must be populated in order to send to Azure. :ivar scopes: List of scopes attached to the token. Required. :vartype scopes: list[str or ~azure.communication.identity.models.CommunicationTokenScope] @@ -162,73 +163,78 @@ class CommunicationIdentityAccessTokenRequest(_Model): # pylint: disable=docstr :vartype expires_in_minutes: int """ - scopes: list[Union[str, "_models.CommunicationTokenScope"]] = rest_field( - visibility=["read", "create", "update", "delete", "query"] - ) - """List of scopes attached to the token. Required.""" - expires_in_minutes: Optional[int] = rest_field( - name="expiresInMinutes", visibility=["read", "create", "update", "delete", "query"] - ) - """Optional custom validity period of the token within [60,1440] minutes range. If not provided, - the default value of 1440 minutes (24 hours) will be used.""" - - @overload + _validation = { + "scopes": {"required": True}, + "expires_in_minutes": {"maximum": 1440, "minimum": 60}, + } + + _attribute_map = { + "scopes": {"key": "scopes", "type": "[str]"}, + "expires_in_minutes": {"key": "expiresInMinutes", "type": "int"}, + } + def __init__( self, *, - scopes: list[Union[str, "_models.CommunicationTokenScope"]], - expires_in_minutes: Optional[int] = None, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + scopes: List[Union[str, "_models.CommunicationTokenScope"]], + expires_in_minutes: int = 1440, + **kwargs: Any + ) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword scopes: List of scopes attached to the token. Required. + :paramtype scopes: list[str or ~azure.communication.identity.models.CommunicationTokenScope] + :keyword expires_in_minutes: Optional custom validity period of the token within [60,1440] + minutes range. If not provided, the default value of 1440 minutes (24 hours) will be used. + :paramtype expires_in_minutes: int """ + super().__init__(**kwargs) + self.scopes = scopes + self.expires_in_minutes = expires_in_minutes - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - -class CommunicationIdentityAccessTokenResult(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only +class CommunicationIdentityAccessTokenResult(_serialization.Model): """A communication identity with access token. - :ivar identity: The communication identity. Required. - :vartype identity: ~azure.communication.identity._generated.models.CommunicationIdentity + All required parameters must be populated in order to send to Azure. + + :ivar identity: A communication identity. Required. + :vartype identity: ~azure.communication.identity.models.CommunicationIdentity :ivar access_token: An access token. - :vartype access_token: - ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :vartype access_token: ~azure.communication.identity.models.CommunicationIdentityAccessToken """ - identity: "_models.CommunicationIdentity" = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """The communication identity. Required.""" - access_token: Optional["_models.CommunicationIdentityAccessToken"] = rest_field( - name="accessToken", visibility=["read", "create", "update", "delete", "query"] - ) - """An access token.""" + _validation = { + "identity": {"required": True}, + } + + _attribute_map = { + "identity": {"key": "identity", "type": "CommunicationIdentity"}, + "access_token": { + "key": "accessToken", + "type": "CommunicationIdentityAccessToken", + }, + } - @overload def __init__( self, *, identity: "_models.CommunicationIdentity", access_token: Optional["_models.CommunicationIdentityAccessToken"] = None, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + **kwargs: Any + ) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword identity: A communication identity. Required. + :paramtype identity: ~azure.communication.identity.models.CommunicationIdentity + :keyword access_token: An access token. + :paramtype access_token: ~azure.communication.identity.models.CommunicationIdentityAccessToken """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + super().__init__(**kwargs) + self.identity = identity + self.access_token = access_token -class CommunicationIdentityCreateRequest(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only - """Request to create a new identity with optional access token. +class CommunicationIdentityCreateRequest(_serialization.Model): + """CommunicationIdentityCreateRequest. :ivar create_token_with_scopes: Also create access token for the created identity. :vartype create_token_with_scopes: list[str or @@ -238,170 +244,76 @@ class CommunicationIdentityCreateRequest(_Model): # pylint: disable=docstring-k :vartype expires_in_minutes: int """ - create_token_with_scopes: Optional[list[Union[str, "_models.CommunicationTokenScope"]]] = rest_field( - name="createTokenWithScopes", visibility=["read", "create", "update", "delete", "query"] - ) - """Also create access token for the created identity.""" - expires_in_minutes: Optional[int] = rest_field( - name="expiresInMinutes", visibility=["read", "create", "update", "delete", "query"] - ) - """Optional custom validity period of the token within [60,1440] minutes range. If not provided, - the default value of 1440 minutes (24 hours) will be used.""" - - @overload - def __init__( - self, - *, - create_token_with_scopes: Optional[list[Union[str, "_models.CommunicationTokenScope"]]] = None, - expires_in_minutes: Optional[int] = None, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: - """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - + _validation = { + "expires_in_minutes": {"maximum": 1440, "minimum": 60}, + } -class TeamsExtensionAssignmentCreateOrUpdateRequest( - _Model -): # pylint: disable=name-too-long,docstring-keyword-should-match-keyword-only - """A request to create or update a Teams Extension assignment. + _attribute_map = { + "create_token_with_scopes": {"key": "createTokenWithScopes", "type": "[str]"}, + "expires_in_minutes": {"key": "expiresInMinutes", "type": "int"}, + } - :ivar principal_type: The type of principal the assignment is for. Required. Known values are: - "resourceAccount" and "user". - :vartype principal_type: str or - ~azure.communication.identity.models.TeamsExtensionPrincipalType - :ivar client_ids: The client IDs for the assignment. - :vartype client_ids: list[str] - """ - - principal_type: Union[str, "_models.TeamsExtensionPrincipalType"] = rest_field( - name="principalType", visibility=["read", "create", "update", "delete", "query"] - ) - """The type of principal the assignment is for. Required. Known values are: \"resourceAccount\" - and \"user\".""" - client_ids: Optional[list[str]] = rest_field( - name="clientIds", visibility=["read", "create", "update", "delete", "query"] - ) - """The client IDs for the assignment.""" - - @overload def __init__( self, *, - principal_type: Union[str, "_models.TeamsExtensionPrincipalType"], - client_ids: Optional[list[str]] = None, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + create_token_with_scopes: Optional[List[Union[str, "_models.CommunicationTokenScope"]]] = None, + expires_in_minutes: int = 1440, + **kwargs: Any + ) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword create_token_with_scopes: Also create access token for the created identity. + :paramtype create_token_with_scopes: list[str or + ~azure.communication.identity.models.CommunicationTokenScope] + :keyword expires_in_minutes: Optional custom validity period of the token within [60,1440] + minutes range. If not provided, the default value of 1440 minutes (24 hours) will be used. + :paramtype expires_in_minutes: int """ + super().__init__(**kwargs) + self.create_token_with_scopes = create_token_with_scopes + self.expires_in_minutes = expires_in_minutes - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) +class TeamsUserExchangeTokenRequest(_serialization.Model): + """TeamsUserExchangeTokenRequest. -class TeamsExtensionAssignmentResponse(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only - """A Teams Extension assignment response. + All required parameters must be populated in order to send to Azure. - :ivar object_id: The object ID of the assignment. Required. - :vartype object_id: str - :ivar tenant_id: The tenant ID of the assignment. Required. - :vartype tenant_id: str - :ivar principal_type: The type of principal the assignment is for. Required. Known values are: - "resourceAccount" and "user". - :vartype principal_type: str or - ~azure.communication.identity.models.TeamsExtensionPrincipalType - :ivar client_ids: The client IDs for the assignment. - :vartype client_ids: list[str] - """ - - object_id: str = rest_field(name="objectId", visibility=["read", "create", "update", "delete", "query"]) - """The object ID of the assignment. Required.""" - tenant_id: str = rest_field(name="tenantId", visibility=["read", "create", "update", "delete", "query"]) - """The tenant ID of the assignment. Required.""" - principal_type: Union[str, "_models.TeamsExtensionPrincipalType"] = rest_field( - name="principalType", visibility=["read", "create", "update", "delete", "query"] - ) - """The type of principal the assignment is for. Required. Known values are: \"resourceAccount\" - and \"user\".""" - client_ids: Optional[list[str]] = rest_field( - name="clientIds", visibility=["read", "create", "update", "delete", "query"] - ) - """The client IDs for the assignment.""" - - @overload - def __init__( - self, - *, - object_id: str, - tenant_id: str, - principal_type: Union[str, "_models.TeamsExtensionPrincipalType"], - client_ids: Optional[list[str]] = None, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: - """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - -class TeamsExtensionExchangeTokenRequest(_Model): - """A request to exchange a Teams Extension token.""" - - -class TeamsUserExchangeTokenRequest(_Model): # pylint: disable=docstring-keyword-should-match-keyword-only - """A request to exchange a Teams user access token. - - :ivar token: Entra ID access token of a Teams User to acquire a new Communication Identity + :ivar token: Azure AD access token of a Teams User to acquire a new Communication Identity access token. Required. :vartype token: str - :ivar app_id: Client ID of an Entra ID application to be verified against the appid claim in - the Entra ID access token. Required. + :ivar app_id: Client ID of an Azure AD application to be verified against the appid claim in + the Azure AD access token. Required. :vartype app_id: str - :ivar user_id: Object ID of an Entra ID user (Teams User) to be verified against the oid claim - in the Entra ID access token. Required. + :ivar user_id: Object ID of an Azure AD user (Teams User) to be verified against the oid claim + in the Azure AD access token. Required. :vartype user_id: str """ - token: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """Entra ID access token of a Teams User to acquire a new Communication Identity access token. - Required.""" - app_id: str = rest_field(name="appId", visibility=["read", "create", "update", "delete", "query"]) - """Client ID of an Entra ID application to be verified against the appid claim in the Entra ID - access token. Required.""" - user_id: str = rest_field(name="userId", visibility=["read", "create", "update", "delete", "query"]) - """Object ID of an Entra ID user (Teams User) to be verified against the oid claim in the Entra ID - access token. Required.""" - - @overload - def __init__( - self, - *, - token: str, - app_id: str, - user_id: str, - ) -> None: ... + _validation = { + "token": {"required": True}, + "app_id": {"required": True}, + "user_id": {"required": True}, + } + + _attribute_map = { + "token": {"key": "token", "type": "str"}, + "app_id": {"key": "appId", "type": "str"}, + "user_id": {"key": "userId", "type": "str"}, + } - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: + def __init__(self, *, token: str, app_id: str, user_id: str, **kwargs: Any) -> None: """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] + :keyword token: Azure AD access token of a Teams User to acquire a new Communication Identity + access token. Required. + :paramtype token: str + :keyword app_id: Client ID of an Azure AD application to be verified against the appid claim in + the Azure AD access token. Required. + :paramtype app_id: str + :keyword user_id: Object ID of an Azure AD user (Teams User) to be verified against the oid + claim in the Azure AD access token. Required. + :paramtype user_id: str """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + super().__init__(**kwargs) + self.token = token + self.app_id = app_id + self.user_id = user_id diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py index ea765788358a..f7dd32510333 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/models/_patch.py @@ -1,14 +1,14 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------- +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ +from typing import List -__all__: list[str] = [] # Add all objects you want publicly available to users at this package level +__all__: List[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py index 46690414aedf..2c8ddc0b3dd9 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/__init__.py @@ -2,28 +2,18 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -# pylint: disable=wrong-import-position -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from ._patch import * # pylint: disable=unused-wildcard-import - -from ._operations import IdentityOperationsOperations # type: ignore -from ._operations import TeamsUserOperationsOperations # type: ignore -from ._operations import TeamsExtensionOperationsOperations # type: ignore +from ._operations import CommunicationIdentityOperations from ._patch import __all__ as _patch_all -from ._patch import * +from ._patch import * # pylint: disable=unused-wildcard-import from ._patch import patch_sdk as _patch_sdk __all__ = [ - "IdentityOperationsOperations", - "TeamsUserOperationsOperations", - "TeamsExtensionOperationsOperations", + "CommunicationIdentityOperations", ] -__all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore +__all__.extend([p for p in _patch_all if p not in __all__]) _patch_sdk() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py index b3495d1ac211..a60d0262ba38 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_operations.py @@ -3,23 +3,18 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. +# Code generated by Microsoft (R) AutoRest Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from collections.abc import MutableMapping from io import IOBase -import json -from typing import Any, Callable, IO, Optional, TypeVar, Union, overload +from typing import Any, Callable, Dict, IO, Optional, TypeVar, Union, overload -from azure.core import PipelineClient from azure.core.exceptions import ( ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, ResourceNotModifiedError, - StreamClosedError, - StreamConsumedError, map_error, ) from azure.core.pipeline import PipelineResponse @@ -27,24 +22,24 @@ from azure.core.tracing.decorator import distributed_trace from azure.core.utils import case_insensitive_dict -from .. import models as _models, types as _types -from .._configuration import IdentityClientConfiguration -from .._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize -from .._utils.serialization import Deserializer, Serializer +from .. import models as _models +from .._serialization import Serializer T = TypeVar("T") -ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, dict[str, Any]], Any]] +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False -def build_identity_operations_create_request(**kwargs: Any) -> HttpRequest: +def build_communication_identity_create_request( + **kwargs: Any, +) -> HttpRequest: # pylint: disable=name-too-long _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -61,10 +56,13 @@ def build_identity_operations_create_request(**kwargs: Any) -> HttpRequest: return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -def build_identity_operations_delete_request(id: str, **kwargs: Any) -> HttpRequest: +def build_communication_identity_delete_request(id: str, **kwargs: Any) -> HttpRequest: # pylint: disable=name-too-long + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) + accept = _headers.pop("Accept", "application/json") + # Construct URL _url = "/identities/{id}" path_format_arguments = { @@ -76,41 +74,23 @@ def build_identity_operations_delete_request(id: str, **kwargs: Any) -> HttpRequ # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - return HttpRequest(method="DELETE", url=_url, params=_params, **kwargs) - - -def build_identity_operations_revoke_access_tokens_request( # pylint: disable=name-too-long - id: str, **kwargs: Any -) -> HttpRequest: - _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) - # Construct URL - _url = "/identities/{id}/:revokeAccessTokens" - path_format_arguments = { - "id": _SERIALIZER.url("id", id, "str"), - } - - _url: str = _url.format(**path_format_arguments) # type: ignore - - # Construct parameters - _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - return HttpRequest(method="POST", url=_url, params=_params, **kwargs) + return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) -def build_identity_operations_issue_access_token_request( # pylint: disable=name-too-long +def build_communication_identity_revoke_access_tokens_request( # pylint: disable=name-too-long id: str, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) accept = _headers.pop("Accept", "application/json") # Construct URL - _url = "/identities/{id}/:issueAccessToken" + _url = "/identities/{id}/:revokeAccessTokens" path_format_arguments = { "id": _SERIALIZER.url("id", id, "str"), } @@ -121,21 +101,19 @@ def build_identity_operations_issue_access_token_request( # pylint: disable=nam _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") # Construct headers - if content_type is not None: - _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -def build_teams_user_operations_exchange_teams_user_access_token_request( # pylint: disable=name-too-long +def build_communication_identity_exchange_teams_user_access_token_request( # pylint: disable=name-too-long **kwargs: Any, ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -152,72 +130,20 @@ def build_teams_user_operations_exchange_teams_user_access_token_request( # pyl return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -def build_teams_extension_operations_exchange_token_request( # pylint: disable=name-too-long - **kwargs: Any, -) -> HttpRequest: - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) - accept = _headers.pop("Accept", "application/json") - - # Construct URL - _url = "/access/teamsExtension/:exchangeAccessToken" - - # Construct parameters - _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - - # Construct headers - if content_type is not None: - _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - - return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) - - -def build_teams_extension_operations_get_assignment_request( # pylint: disable=name-too-long - tenant_id: str, object_id: str, **kwargs: Any -) -> HttpRequest: - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) - accept = _headers.pop("Accept", "application/json") - - # Construct URL - _url = "/access/teamsExtension/tenants/{tenantId}/assignments/{objectId}" - path_format_arguments = { - "tenantId": _SERIALIZER.url("tenant_id", tenant_id, "str"), - "objectId": _SERIALIZER.url("object_id", object_id, "str"), - } - - _url: str = _url.format(**path_format_arguments) # type: ignore - - # Construct parameters - _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - - # Construct headers - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - - return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) - - -def build_teams_extension_operations_upsert_assignment_request( # pylint: disable=name-too-long - tenant_id: str, object_id: str, **kwargs: Any +def build_communication_identity_issue_access_token_request( # pylint: disable=name-too-long + id: str, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2023-10-01")) accept = _headers.pop("Accept", "application/json") # Construct URL - _url = "/access/teamsExtension/tenants/{tenantId}/assignments/{objectId}" + _url = "/identities/{id}/:issueAccessToken" path_format_arguments = { - "tenantId": _SERIALIZER.url("tenant_id", tenant_id, "str"), - "objectId": _SERIALIZER.url("object_id", object_id, "str"), + "id": _SERIALIZER.url("id", id, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -230,46 +156,27 @@ def build_teams_extension_operations_upsert_assignment_request( # pylint: disab _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) - - -def build_teams_extension_operations_delete_assignment_request( # pylint: disable=name-too-long - tenant_id: str, object_id: str, **kwargs: Any -) -> HttpRequest: - _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-09-23")) - # Construct URL - _url = "/access/teamsExtension/tenants/{tenantId}/assignments/{objectId}" - path_format_arguments = { - "tenantId": _SERIALIZER.url("tenant_id", tenant_id, "str"), - "objectId": _SERIALIZER.url("object_id", object_id, "str"), - } - - _url: str = _url.format(**path_format_arguments) # type: ignore - - # Construct parameters - _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - - return HttpRequest(method="DELETE", url=_url, params=_params, **kwargs) + return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -class IdentityOperationsOperations: # pylint: disable=docstring-missing-param +class CommunicationIdentityOperations: """ .. warning:: **DO NOT** instantiate this class directly. Instead, you should access the following operations through - :class:`~azure.communication.identity.IdentityClient`'s - :attr:`identity_operations` attribute. + :class:`~azure.communication.identity.CommunicationIdentityClient`'s + :attr:`communication_identity` attribute. """ - def __init__(self, *args, **kwargs) -> None: + models = _models + + def __init__(self, *args, **kwargs): input_args = list(args) - self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") - self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = input_args.pop(0) if input_args else kwargs.pop("deserializer") @overload def create( @@ -285,20 +192,19 @@ def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest + :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @overload def create( self, - body: Optional[_types.CommunicationIdentityCreateRequest] = None, + body: Optional[IO] = None, *, content_type: str = "application/json", **kwargs: Any, @@ -309,42 +215,19 @@ def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Default value is None. - :type body: ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - def create( - self, body: Optional[IO[bytes]] = None, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Create a new identity, and optionally, an access token. - - Create a new identity, and optionally, an access token. - - :param body: If specified, creates also a Communication Identity access token associated with - the identity and containing the requested scopes. Default value is None. - :type body: IO[bytes] + :type body: IO :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessTokenResult + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace def create( self, - body: Optional[ - Union[_models.CommunicationIdentityCreateRequest, _types.CommunicationIdentityCreateRequest, IO[bytes]] - ] = None, + body: Optional[Union[_models.CommunicationIdentityCreateRequest, IO]] = None, **kwargs: Any, ) -> _models.CommunicationIdentityAccessTokenResult: """Create a new identity, and optionally, an access token. @@ -353,16 +236,16 @@ def create( :param body: If specified, creates also a Communication Identity access token associated with the identity and containing the requested scopes. Is either a - CommunicationIdentityCreateRequest type or a IO[bytes] type. Default value is None. - :type body: ~azure.communication.identity._generated.models.CommunicationIdentityCreateRequest - or ~azure.communication.identity._generated.types.CommunicationIdentityCreateRequest or - IO[bytes] - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + CommunicationIdentityCreateRequest type or a IO type. Default value is None. + :type body: ~azure.communication.identity.models.CommunicationIdentityCreateRequest or IO + :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. + Default value is None. + :paramtype content_type: str + :return: CommunicationIdentityAccessTokenResult + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessTokenResult :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -374,22 +257,23 @@ def create( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - content_type = content_type if body else None cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) - content_type = content_type or "application/json" if body else None + content_type = content_type or "application/json" + _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: if body is not None: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + _json = self._serialize.body(body, "CommunicationIdentityCreateRequest") else: - _content = None + _json = None - _request = build_identity_operations_create_request( + request = build_communication_identity_create_request( content_type=content_type, api_version=self._config.api_version, + json=_json, content=_content, headers=_headers, params=_params, @@ -397,38 +281,28 @@ def create( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [201]: if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) + deserialized = self._deserialize("CommunicationIdentityAccessTokenResult", pipeline_response) if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore + return cls(pipeline_response, deserialized, {}) - return deserialized # type: ignore + return deserialized @distributed_trace def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements @@ -442,7 +316,7 @@ def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsisten :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -455,7 +329,7 @@ def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsisten cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_identity_operations_delete_request( + request = build_communication_identity_delete_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -464,25 +338,24 @@ def delete(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsisten path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: + if _stream: + response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) if cls: - return cls(pipeline_response, None, {}) # type: ignore + return cls(pipeline_response, None, {}) @distributed_trace def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements @@ -496,7 +369,7 @@ def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disab :rtype: None :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -509,7 +382,7 @@ def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disab cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_identity_operations_revoke_access_tokens_request( + request = build_communication_identity_revoke_access_tokens_request( id=id, api_version=self._config.api_version, headers=_headers, @@ -518,454 +391,90 @@ def revoke_access_tokens(self, id: str, **kwargs: Any) -> None: # pylint: disab path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [204]: + if _stream: + response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) if cls: - return cls(pipeline_response, None, {}) # type: ignore - - @overload - def issue_access_token( - self, - id: str, - body: _models.CommunicationIdentityAccessTokenRequest, - *, - content_type: str = "application/json", - **kwargs: Any, - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: - ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ + return cls(pipeline_response, None, {}) @overload - def issue_access_token( + def exchange_teams_user_access_token( self, - id: str, - body: _types.CommunicationIdentityAccessTokenRequest, + body: _models.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any, ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: - ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - def issue_access_token( - self, id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Required. - :type body: IO[bytes] - :keyword content_type: Body Parameter content-type. Content type parameter for binary body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @distributed_trace - def issue_access_token( - self, - id: str, - body: Union[ - _models.CommunicationIdentityAccessTokenRequest, _types.CommunicationIdentityAccessTokenRequest, IO[bytes] - ], - **kwargs: Any, - ) -> _models.CommunicationIdentityAccessToken: - """Issue a new token for an identity. - - Issue a new token for an identity. - - :param id: Identifier of the identity to issue token for. Required. - :type id: str - :param body: Requested scopes for the new token. Is either a - CommunicationIdentityAccessTokenRequest type or a IO[bytes] type. Required. - :type body: - ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenRequest or - ~azure.communication.identity._generated.types.CommunicationIdentityAccessTokenRequest or - IO[bytes] - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = kwargs.pop("params", {}) or {} - - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) - - content_type = content_type or "application/json" - _content = None - if isinstance(body, (IOBase, bytes)): - _content = body - else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - - _request = build_identity_operations_issue_access_token_request( - id=id, - content_type=content_type, - api_version=self._config.api_version, - content=_content, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) + """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. - response = pipeline_response.http_response - - if response.status_code not in [200]: - if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) - - if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - -class TeamsUserOperationsOperations: # pylint: disable=docstring-missing-param - """ - .. warning:: - **DO NOT** instantiate this class directly. - - Instead, you should access the following operations through - :class:`~azure.communication.identity.IdentityClient`'s - :attr:`teams_user_operations` attribute. - """ - - def __init__(self, *args, **kwargs) -> None: - input_args = list(args) - self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") - self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") - - @overload - def exchange_teams_user_access_token( - self, body: _models.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. - - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. - - :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - def exchange_teams_user_access_token( - self, body: _types.TeamsUserExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. - - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest + :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @overload def exchange_teams_user_access_token( - self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + self, body: IO, *, content_type: str = "application/json", **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. :param body: Request payload for the token exchange. Required. - :type body: IO[bytes] + :type body: IO :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace def exchange_teams_user_access_token( - self, - body: Union[_models.TeamsUserExchangeTokenRequest, _types.TeamsUserExchangeTokenRequest, IO[bytes]], - **kwargs: Any, + self, body: Union[_models.TeamsUserExchangeTokenRequest, IO], **kwargs: Any ) -> _models.CommunicationIdentityAccessToken: - """Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + """Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. - Exchange an Entra ID access token of a Teams user for a new Communication Identity access token - with a matching expiration time. + Exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new + Communication Identity access token with a matching expiration time. :param body: Request payload for the token exchange. Is either a TeamsUserExchangeTokenRequest - type or a IO[bytes] type. Required. - :type body: ~azure.communication.identity._generated.models.TeamsUserExchangeTokenRequest or - ~azure.communication.identity._generated.types.TeamsUserExchangeTokenRequest or IO[bytes] - :return: CommunicationIdentityAccessToken. The CommunicationIdentityAccessToken is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = kwargs.pop("params", {}) or {} - - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) - - content_type = content_type or "application/json" - _content = None - if isinstance(body, (IOBase, bytes)): - _content = body - else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - - _request = build_teams_user_operations_exchange_teams_user_access_token_request( - content_type=content_type, - api_version=self._config.api_version, - content=_content, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: - if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessToken, response.json()) - - if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - -class TeamsExtensionOperationsOperations: # pylint: disable=docstring-missing-param - """ - .. warning:: - **DO NOT** instantiate this class directly. - - Instead, you should access the following operations through - :class:`~azure.communication.identity.IdentityClient`'s - :attr:`teams_extension_operations` attribute. - """ - - def __init__(self, *args, **kwargs) -> None: - input_args = list(args) - self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") - self._config: IdentityClientConfiguration = input_args.pop(0) if input_args else kwargs.pop("config") - self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") - self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") - - @overload - def exchange_token( - self, body: _models.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. - - Exchanges a Teams Phone token for an ACS user access token. - - :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - def exchange_token( - self, body: _types.TeamsExtensionExchangeTokenRequest, *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. - - Exchanges a Teams Phone token for an ACS user access token. - - :param body: Request payload for the token exchange. Required. - :type body: ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - - @overload - def exchange_token( - self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. - - Exchanges a Teams Phone token for an ACS user access token. - - :param body: Request payload for the token exchange. Required. - :type body: IO[bytes] - :keyword content_type: Body Parameter content-type. Content type parameter for binary body. - Default value is "application/json". + type or a IO type. Required. + :type body: ~azure.communication.identity.models.TeamsUserExchangeTokenRequest or IO + :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. + Default value is None. :paramtype content_type: str - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - - @distributed_trace - def exchange_token( - self, - body: Union[_models.TeamsExtensionExchangeTokenRequest, _types.TeamsExtensionExchangeTokenRequest, IO[bytes]], - **kwargs: Any, - ) -> _models.CommunicationIdentityAccessTokenResult: - """Exchanges a Teams Phone token for an ACS user access token. - - Exchanges a Teams Phone token for an ACS user access token. - - :param body: Request payload for the token exchange. Is either a - TeamsExtensionExchangeTokenRequest type or a IO[bytes] type. Required. - :type body: ~azure.communication.identity._generated.models.TeamsExtensionExchangeTokenRequest - or ~azure.communication.identity._generated.types.TeamsExtensionExchangeTokenRequest or - IO[bytes] - :return: CommunicationIdentityAccessTokenResult. The CommunicationIdentityAccessTokenResult is - compatible with MutableMapping - :rtype: ~azure.communication.identity._generated.models.CommunicationIdentityAccessTokenResult - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -977,18 +486,20 @@ def exchange_token( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.CommunicationIdentityAccessTokenResult] = kwargs.pop("cls", None) + cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) content_type = content_type or "application/json" + _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + _json = self._serialize.body(body, "TeamsUserExchangeTokenRequest") - _request = build_teams_extension_operations_exchange_token_request( + request = build_communication_identity_exchange_teams_user_access_token_request( content_type=content_type, api_version=self._config.api_version, + json=_json, content=_content, headers=_headers, params=_params, @@ -996,225 +507,103 @@ def exchange_token( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response + request.url = self._client.format_url(request.url, **path_format_arguments) - if response.status_code not in [200]: - if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.CommunicationIdentityAccessTokenResult, response.json()) - - if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - @distributed_trace - def get_assignment(self, tenant_id: str, object_id: str, **kwargs: Any) -> _models.TeamsExtensionAssignmentResponse: - """Get Teams Phone access assignment by object id. - - Get Teams Phone access assignment by object id. - - :param tenant_id: Tenant Id we want to get the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to get the assignment for. Required. - :type object_id: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) - - _request = build_teams_extension_operations_get_assignment_request( - tenant_id=tenant_id, - object_id=object_id, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response if response.status_code not in [200]: if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) + deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore + return cls(pipeline_response, deserialized, {}) - return deserialized # type: ignore + return deserialized @overload - def upsert_assignment( + def issue_access_token( self, - tenant_id: str, - object_id: str, - body: _models.TeamsExtensionAssignmentCreateOrUpdateRequest, + id: str, + body: _models.CommunicationIdentityAccessTokenRequest, *, content_type: str = "application/json", **kwargs: Any, - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Required. - :type body: - ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @overload - def upsert_assignment( + def issue_access_token( self, - tenant_id: str, - object_id: str, - body: _types.TeamsExtensionAssignmentCreateOrUpdateRequest, + id: str, + body: IO, *, content_type: str = "application/json", **kwargs: Any, - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Required. - :type body: - ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest - :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. - Default value is "application/json". - :paramtype content_type: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse - :raises ~azure.core.exceptions.HttpResponseError: - """ + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. - @overload - def upsert_assignment( - self, tenant_id: str, object_id: str, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Required. - :type body: IO[bytes] + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Required. + :type body: IO :keyword content_type: Body Parameter content-type. Content type parameter for binary body. Default value is "application/json". :paramtype content_type: str - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ @distributed_trace - def upsert_assignment( + def issue_access_token( self, - tenant_id: str, - object_id: str, - body: Union[ - _models.TeamsExtensionAssignmentCreateOrUpdateRequest, - _types.TeamsExtensionAssignmentCreateOrUpdateRequest, - IO[bytes], - ], + id: str, + body: Union[_models.CommunicationIdentityAccessTokenRequest, IO], **kwargs: Any, - ) -> _models.TeamsExtensionAssignmentResponse: - """Creates or replaces a Teams Phone access assignment. - - Creates or replaces a Teams Phone access assignment. - - :param tenant_id: Tenant Id we want to update the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to update the assignment for. Required. - :type object_id: str - :param body: Values we want to set. Is either a TeamsExtensionAssignmentCreateOrUpdateRequest - type or a IO[bytes] type. Required. - :type body: - ~azure.communication.identity._generated.models.TeamsExtensionAssignmentCreateOrUpdateRequest - or ~azure.communication.identity._generated.types.TeamsExtensionAssignmentCreateOrUpdateRequest - or IO[bytes] - :return: TeamsExtensionAssignmentResponse. The TeamsExtensionAssignmentResponse is compatible - with MutableMapping - :rtype: ~azure.communication.identity._generated.models.TeamsExtensionAssignmentResponse + ) -> _models.CommunicationIdentityAccessToken: + """Issue a new token for an identity. + + Issue a new token for an identity. + + :param id: Identifier of the identity to issue token for. Required. + :type id: str + :param body: Requested scopes for the new token. Is either a + CommunicationIdentityAccessTokenRequest type or a IO type. Required. + :type body: ~azure.communication.identity.models.CommunicationIdentityAccessTokenRequest or IO + :keyword content_type: Body Parameter content-type. Known values are: 'application/json'. + Default value is None. + :paramtype content_type: str + :return: CommunicationIdentityAccessToken + :rtype: ~azure.communication.identity.models.CommunicationIdentityAccessToken :raises ~azure.core.exceptions.HttpResponseError: """ - error_map: MutableMapping = { + error_map = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, @@ -1226,20 +615,21 @@ def upsert_assignment( _params = kwargs.pop("params", {}) or {} content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - cls: ClsType[_models.TeamsExtensionAssignmentResponse] = kwargs.pop("cls", None) + cls: ClsType[_models.CommunicationIdentityAccessToken] = kwargs.pop("cls", None) content_type = content_type or "application/json" + _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + _json = self._serialize.body(body, "CommunicationIdentityAccessTokenRequest") - _request = build_teams_extension_operations_upsert_assignment_request( - tenant_id=tenant_id, - object_id=object_id, + request = build_communication_identity_issue_access_token_request( + id=id, content_type=content_type, api_version=self._config.api_version, + json=_json, content=_content, headers=_headers, params=_params, @@ -1247,94 +637,25 @@ def upsert_assignment( path_format_arguments = { "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + request.url = self._client.format_url(request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs + request, stream=_stream, **kwargs ) response = pipeline_response.http_response - if response.status_code not in [200, 201]: + if response.status_code not in [200]: if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + response.read() # Load the body in memory and close the socket map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) + error = self._deserialize.failsafe_deserialize(_models.CommunicationErrorResponse, pipeline_response) raise HttpResponseError(response=response, model=error) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.TeamsExtensionAssignmentResponse, response.json()) + deserialized = self._deserialize("CommunicationIdentityAccessToken", pipeline_response) if cls: - return cls(pipeline_response, deserialized, {}) # type: ignore - - return deserialized # type: ignore - - @distributed_trace - def delete_assignment( # pylint: disable=inconsistent-return-statements - self, tenant_id: str, object_id: str, **kwargs: Any - ) -> None: - """Removes a Teams Phone access assignment. - - Removes a Teams Phone access assignment. + return cls(pipeline_response, deserialized, {}) - :param tenant_id: Tenant Id we want to remove the assignment for. Required. - :type tenant_id: str - :param object_id: Object Id we want to remove the assignment for. Required. - :type object_id: str - :return: None - :rtype: None - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[None] = kwargs.pop("cls", None) - - _request = build_teams_extension_operations_delete_assignment_request( - tenant_id=tenant_id, - object_id=object_id, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _stream = False - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [204]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.CommunicationErrorResponse, - response, - ) - raise HttpResponseError(response=response, model=error) - - if cls: - return cls(pipeline_response, None, {}) # type: ignore + return deserialized diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py index ea765788358a..f7dd32510333 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/operations/_patch.py @@ -1,14 +1,14 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------- +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ +from typing import List -__all__: list[str] = [] # Add all objects you want publicly available to users at this package level +__all__: List[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed new file mode 100644 index 000000000000..e5aff4f83af8 --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py deleted file mode 100644 index 3eb0053aa283..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_generated/types.py +++ /dev/null @@ -1,93 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - -from typing import TYPE_CHECKING, Union -from typing_extensions import Required, TypedDict - -if TYPE_CHECKING: - from .models import CommunicationTokenScope, TeamsExtensionPrincipalType - - -class CommunicationIdentityAccessTokenRequest(TypedDict, total=False): - """Request to issue a new access token for an identity. - - :ivar scopes: List of scopes attached to the token. Required. - :vartype scopes: list[Union[str, "CommunicationTokenScope"]] - :ivar expiresInMinutes: Optional custom validity period of the token within [60,1440] minutes - range. If not provided, the default value of 1440 minutes (24 hours) will be used. - :vartype expiresInMinutes: int - """ - - scopes: Required[list[Union[str, "CommunicationTokenScope"]]] - """List of scopes attached to the token. Required.""" - expiresInMinutes: int - """Optional custom validity period of the token within [60,1440] minutes range. If not provided, - the default value of 1440 minutes (24 hours) will be used.""" - - -class CommunicationIdentityCreateRequest(TypedDict, total=False): - """Request to create a new identity with optional access token. - - :ivar createTokenWithScopes: Also create access token for the created identity. - :vartype createTokenWithScopes: list[Union[str, "CommunicationTokenScope"]] - :ivar expiresInMinutes: Optional custom validity period of the token within [60,1440] minutes - range. If not provided, the default value of 1440 minutes (24 hours) will be used. - :vartype expiresInMinutes: int - """ - - createTokenWithScopes: list[Union[str, "CommunicationTokenScope"]] - """Also create access token for the created identity.""" - expiresInMinutes: int - """Optional custom validity period of the token within [60,1440] minutes range. If not provided, - the default value of 1440 minutes (24 hours) will be used.""" - - -class TeamsExtensionAssignmentCreateOrUpdateRequest(TypedDict, total=False): # pylint: disable=name-too-long - """A request to create or update a Teams Extension assignment. - - :ivar principalType: The type of principal the assignment is for. Required. Known values are: - "resourceAccount" and "user". - :vartype principalType: Union[str, "TeamsExtensionPrincipalType"] - :ivar clientIds: The client IDs for the assignment. - :vartype clientIds: list[str] - """ - - principalType: Required[Union[str, "TeamsExtensionPrincipalType"]] - """The type of principal the assignment is for. Required. Known values are: \"resourceAccount\" - and \"user\".""" - clientIds: list[str] - """The client IDs for the assignment.""" - - -class TeamsExtensionExchangeTokenRequest(TypedDict, total=False): - """A request to exchange a Teams Extension token.""" - - -class TeamsUserExchangeTokenRequest(TypedDict, total=False): - """A request to exchange a Teams user access token. - - :ivar token: Entra ID access token of a Teams User to acquire a new Communication Identity - access token. Required. - :vartype token: str - :ivar appId: Client ID of an Entra ID application to be verified against the appid claim in the - Entra ID access token. Required. - :vartype appId: str - :ivar userId: Object ID of an Entra ID user (Teams User) to be verified against the oid claim - in the Entra ID access token. Required. - :vartype userId: str - """ - - token: Required[str] - """Entra ID access token of a Teams User to acquire a new Communication Identity access token. - Required.""" - appId: Required[str] - """Client ID of an Entra ID application to be verified against the appid claim in the Entra ID - access token. Required.""" - userId: Required[str] - """Object ID of an Entra ID user (Teams User) to be verified against the oid claim in the Entra ID - access token. Required.""" diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py index ca784b76e0b6..0d6042f38bcf 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_utils.py @@ -5,174 +5,7 @@ # ------------------------------------------------------------------------- from datetime import timedelta -from typing import Any, Dict, List, Optional, Union -from azure.core.credentials import AccessToken -from azure.core.pipeline.policies import SansIOHTTPPolicy - -# Headers the AutoRest-generated client sent that the TypeSpec DPG emitter does not. -# -# Restoring them keeps the request black-box identical to the previously published SDK. The -# service itself is indifferent -- measured against a live resource, `Accept` makes no -# difference to either the success or the error path -- but a proxy, gateway or request log -# keying on headers would observe the change, so the difference is customer-visible even -# though the service ignores it. -# -# Why the emitter drops them: -# - `delete` and `revoke_access_tokens` return 204 with no response body, so the emitter -# emits no `Accept` header for them at all. -# - `create` declares `content_type` only as a body-parameter keyword, so with no body -# passed it is never applied. -# -# These are request headers only. Sending `Content-Type` does not cause a body to be sent; -# the body remains absent for `create_user`, as it was under AutoRest. -_ACCEPT_JSON = {"Accept": "application/json"} -_CONTENT_TYPE_JSON = {"Content-Type": "application/json"} - - -def to_access_token(access_token: Any) -> AccessToken: - """Build an AccessToken from a generated CommunicationIdentityAccessToken. - - ``expiresOn`` is read **by key**, not via the ``.expires_on`` attribute. Do not - "simplify" this to attribute access: the two are not equivalent. - - The TypeSpec model declares ``expiresOn: utcDateTime``, so the generated model types it - as ``datetime.datetime`` and attribute access returns a deserialized ``datetime``. Every - previously published version of this SDK exposed the raw service string, so returning a - ``datetime`` would be a breaking change for callers. Item access on the generated model - returns the untouched wire value, which keeps the public surface unchanged. - - This is a re-occurrence, not a new concern. The AutoRest configuration formerly at - ``swagger/SWAGGER.md`` carried a directive deleting ``format: date-time`` from this same - property, added because a generator upgrade had already turned it from string into - ``datetime`` once before. That directive acted on the swagger document and does not carry - over to TypeSpec, so this function is now the only thing preventing the same break. - - ``.isoformat()`` is **not** an adequate substitute: it reformats the value (``Z`` becomes - ``+00:00``) and silently truncates the service's 7-digit fractional seconds to 6. - - :param access_token: A generated CommunicationIdentityAccessToken instance. - :type access_token: ~azure.communication.identity._generated.models.CommunicationIdentityAccessToken - :return: The access token and its expiry, as returned by the service. - :rtype: ~azure.core.credentials.AccessToken - """ - return AccessToken(access_token["token"], access_token["expiresOn"]) - - -def build_token_request_body( - scopes_key: str, - scopes: List[Union[str, Any]], - token_expires_in: Optional[timedelta], -) -> Dict[str, Any]: - """Build a token request body, omitting fields the caller did not supply. - - The previous msrest-based layer dropped ``None`` fields during serialization, whereas the - generated code forwards the request body as-is. Both ``scopes`` and ``expiresInMinutes`` - are therefore omitted when unset, so the request stays byte-identical to the previously - published SDK. - - ``expiresInMinutes`` matters because the service constrains it to [60, 1440] and an - explicit ``null`` is not a valid value. ``scopes`` is omitted for the same reason the old - layer omitted it -- passing ``None`` for a required argument is a caller error either way, - but it must fail the way it always failed, with the same request on the wire. - - Note this restores prior behaviour rather than adding validation: the AutoRest client did - not raise on a ``None`` scope list, and neither does this. The request is still sent and - the service still rejects it. - - :param scopes_key: Name of the scopes property expected by the service. - :type scopes_key: str - :param scopes: List of scopes to be added to the token. - :type scopes: list[str or ~azure.communication.identity.CommunicationTokenScope] - :param token_expires_in: Optional custom validity period of the token. - :type token_expires_in: ~datetime.timedelta or None - :return: The request body to send to the service. - :rtype: dict[str, any] - """ - request_body: Dict[str, Any] = {} - if scopes is not None: - request_body[scopes_key] = scopes - expires_in_minutes = convert_timedelta_to_mins(token_expires_in) - if expires_in_minutes is not None: - request_body["expiresInMinutes"] = expires_in_minutes - return request_body - - -def extract_create_content_type(kwargs: Dict[str, Any]) -> Dict[str, Any]: - """Preserve a caller-supplied ``Content-Type`` across the generated create operation. - - The operation discards it when no body is present, so it is moved into the - request context for :class:`BodylessCreateContentTypePolicy` to reapply. - - :param kwargs: Keyword arguments destined for the create operation. - :type kwargs: dict[str, any] - :return: The keyword arguments, with any Content-Type moved to the context. - :rtype: dict[str, any] - """ - headers = kwargs.get("headers") or {} - override = kwargs.pop("content_type", None) - for name in list(headers): - if name.lower() == "content-type": - override = headers.pop(name) - if override: - kwargs[BodylessCreateContentTypePolicy.CONTEXT_KEY] = override - return kwargs - - -def merge_headers(kwargs: Dict[str, Any], defaults: Dict[str, str]) -> Dict[str, Any]: - """Add default request headers without overriding any the caller supplied. - - :param kwargs: Keyword arguments destined for a generated operation. - :type kwargs: dict[str, any] - :param defaults: Headers to apply when the caller has not set them. - :type defaults: dict[str, str] - :return: The keyword arguments, with a merged ``headers`` entry. - :rtype: dict[str, any] - """ - headers = dict(kwargs.pop("headers", None) or {}) - existing = {name.lower() for name in headers} - for name, value in defaults.items(): - if name.lower() not in existing: - headers[name] = value - kwargs["headers"] = headers - return kwargs - - -class BodylessCreateContentTypePolicy(SansIOHTTPPolicy): - """Restores ``Content-Type`` on the bodyless identity-create request. - - The generated ``create`` operation discards ``content_type`` whenever no body - is present -- twice, via ``content_type if body else None`` and again via - ``content_type or "application/json" if body else None`` -- so no argument - passed to the operation can survive. A policy is therefore the only way to - restore the header without editing generated code. - - The same discard also drops a caller-supplied ``Content-Type``, which the - AutoRest client honoured. ``CommunicationIdentityClient.create_user`` - consequently stashes any caller value on the request context under - ``_acs_create_content_type`` and this policy reapplies it, so an explicit - override still reaches the wire. - - The match is deliberately narrow: a POST to the identities collection with no - body. ``create_user_and_token`` sends a body and so already carries the header - from the generated code, and is left untouched. - """ - - CONTEXT_KEY = "_acs_create_content_type" - - def on_request(self, request) -> None: - http_request = request.http_request - if http_request.method != "POST": - return - if not http_request.url.split("?")[0].endswith("/identities"): - return - # The generated operation drops content_type whenever the body is falsy, which covers - # both no body at all and an empty JSON object. Both must be repaired, so the test is - # "did the operation fail to set a JSON content type", not "is there a body" -- an - # empty dict serializes to the string "{}", which is truthy and would be missed. - if http_request.headers.get("Content-Type", "").startswith("application/json"): - return - override = request.context.options.pop(self.CONTEXT_KEY, None) - http_request.headers["Content-Type"] = override or "application/json" +from typing import Optional def convert_timedelta_to_mins( diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py index 563541b7b0a4..fa4bba0a08c4 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py @@ -4,6 +4,6 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "2.0.0" +VERSION = "1.6.0" SDK_MONIKER = f"communication-identity/{VERSION}" # type: str diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py index 6fdc6bc78932..466f314d6673 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -11,21 +11,14 @@ from azure.core.credentials_async import AsyncTokenCredential from azure.core.credentials import AzureKeyCredential from .._generated.aio._client import ( - IdentityClient as CommunicationIdentityClientGen, + CommunicationIdentityClient as CommunicationIdentityClientGen, ) from .._shared.auth_policy_utils import get_authentication_policy from .._shared.utils import parse_connection_str from .._shared.models import CommunicationUserIdentifier from .._version import SDK_MONIKER from .._api_versions import DEFAULT_VERSION -from .._utils import ( - _ACCEPT_JSON, - BodylessCreateContentTypePolicy, - build_token_request_body, - extract_create_content_type, - merge_headers, - to_access_token, -) +from .._utils import convert_timedelta_to_mins from .._generated.models import CommunicationTokenScope @@ -58,14 +51,11 @@ def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureK self._endpoint = endpoint self._api_version = kwargs.pop("api_version", DEFAULT_VERSION) - per_call_policies = list(kwargs.pop("per_call_policies", None) or []) - per_call_policies.append(BodylessCreateContentTypePolicy()) self._identity_service_client = CommunicationIdentityClientGen( self._endpoint, api_version=self._api_version, authentication_policy=get_authentication_policy(endpoint, credential, decode_url=True, is_async=True), sdk_moniker=SDK_MONIKER, - per_call_policies=per_call_policies, **kwargs ) @@ -91,17 +81,13 @@ async def create_user(self, **kwargs) -> "CommunicationUserIdentifier": :return: CommunicationUserIdentifier :rtype: ~azure.communication.identity.CommunicationUserIdentifier """ - identity_access_token = await self._identity_service_client.identity_operations.create( - **extract_create_content_type(kwargs) - ) + identity_access_token = await self._identity_service_client.communication_identity.create(**kwargs) return CommunicationUserIdentifier(identity_access_token.identity.id, raw_id=identity_access_token.identity.id) @distributed_trace_async async def create_user_and_token( - self, - scopes: List[Union[str, CommunicationTokenScope]], - *, + self, scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> Tuple["CommunicationUserIdentifier", AccessToken]: @@ -117,15 +103,22 @@ async def create_user_and_token( :rtype: tuple of (~azure.communication.identity.CommunicationUserIdentifier, ~azure.core.credentials.AccessToken) """ - request_body = build_token_request_body("createTokenWithScopes", scopes, token_expires_in) - identity_access_token = await self._identity_service_client.identity_operations.create( + request_body = { + "createTokenWithScopes": scopes, + "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), + } + + identity_access_token = await self._identity_service_client.communication_identity.create( body=request_body, **kwargs # type: ignore ) user_identifier = CommunicationUserIdentifier( identity_access_token.identity.id, raw_id=identity_access_token.identity.id ) - access_token = to_access_token(identity_access_token["accessToken"]) + access_token = AccessToken( + identity_access_token.access_token.token, + identity_access_token.access_token.expires_on, + ) return user_identifier, access_token @@ -139,16 +132,12 @@ async def delete_user(self, user: CommunicationUserIdentifier, **kwargs) -> None :return: None :rtype: None """ - await self._identity_service_client.identity_operations.delete( - user.properties["id"], **merge_headers(kwargs, _ACCEPT_JSON) - ) + await self._identity_service_client.communication_identity.delete(user.properties["id"], **kwargs) @distributed_trace_async async def get_token( - self, - user: CommunicationUserIdentifier, - scopes: List[Union[str, CommunicationTokenScope]], - *, + self, user: CommunicationUserIdentifier, + scopes: List[Union[str, CommunicationTokenScope]], *, token_expires_in: Optional[timedelta] = None, **kwargs ) -> AccessToken: @@ -164,13 +153,16 @@ async def get_token( :return: AccessToken :rtype: ~azure.core.credentials.AccessToken """ - request_body = build_token_request_body("scopes", scopes, token_expires_in) + request_body = { + "scopes": scopes, + "expiresInMinutes": convert_timedelta_to_mins(token_expires_in), + } - access_token = await self._identity_service_client.identity_operations.issue_access_token( + access_token = await self._identity_service_client.communication_identity.issue_access_token( user.properties["id"], body=request_body, **kwargs # type: ignore ) - return to_access_token(access_token) + return AccessToken(access_token.token, access_token.expires_on) @distributed_trace_async async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> None: @@ -181,8 +173,8 @@ async def revoke_tokens(self, user: CommunicationUserIdentifier, **kwargs) -> No :return: None :rtype: None """ - return await self._identity_service_client.identity_operations.revoke_access_tokens( - user.properties["id"] if user else None, **merge_headers(kwargs, _ACCEPT_JSON) # type: ignore + return await self._identity_service_client.communication_identity.revoke_access_tokens( + user.properties["id"] if user else None, **kwargs # type: ignore ) @distributed_trace_async @@ -212,11 +204,11 @@ async def get_token_for_teams_user( "appId": client_id, "userId": user_object_id, } - access_token = await self._identity_service_client.teams_user_operations.exchange_teams_user_access_token( + access_token = await self._identity_service_client.communication_identity.exchange_teams_user_access_token( body=request_body, **kwargs # type: ignore ) - return to_access_token(access_token) + return AccessToken(access_token.token, access_token.expires_on) async def __aenter__(self) -> "CommunicationIdentityClient": await self._identity_service_client.__aenter__() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed b/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed index e5aff4f83af8..e69de29bb2d1 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/py.typed @@ -1 +0,0 @@ -# Marker file for PEP 561. \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/pyproject.toml b/sdk/communication/azure-communication-identity/pyproject.toml index d85b497d8daa..db898be6409d 100644 --- a/sdk/communication/azure-communication-identity/pyproject.toml +++ b/sdk/communication/azure-communication-identity/pyproject.toml @@ -1,83 +1,9 @@ -[build-system] -requires = [ - "setuptools>=77.0.3", - "wheel", -] -build-backend = "setuptools.build_meta" - -[project] -name = "azure-communication-identity" -authors = [ - { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" }, -] -description = "Microsoft Corporation Azure Communication Identity Client Library for Python" -license = "MIT" -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", -] -requires-python = ">=3.10" -keywords = [ - "azure", - "azure sdk", -] -dependencies = [ - "isodate>=0.6.1", - "azure-core>=1.37.0", - "typing-extensions>=4.6.0", -] -dynamic = [ - "version", - "readme", -] - -[project.urls] -repository = "https://github.com/Azure/azure-sdk-for-python" - -[tool.setuptools.dynamic.version] -attr = "azure.communication.identity._version.VERSION" - -[tool.setuptools.dynamic.readme] -file = [ - "README.md", - "CHANGELOG.md", -] -content-type = "text/markdown" - -[tool.setuptools.packages.find] -exclude = [ - "tests*", - "generated_tests*", - "samples*", - "generated_samples*", - "doc*", - "azure", - "azure.communication", -] - -[tool.setuptools.package-data] -pytyped = [ - "py.typed", -] - [tool.azure-sdk-build] pyright = false +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" + [tool.azure-sdk-conda] in_bundle = true bundle_name = "azure-communication" - -[packaging] -auto_update = false -package_name = "azure-communication-identity" -package_pprint_name = "Communication Identity Service" -package_doc_id = "" -is_stable = false -is_arm = false diff --git a/sdk/communication/azure-communication-identity/samples/identity_samples.py b/sdk/communication/azure-communication-identity/samples/identity_samples.py index 3c380a935169..781afd26041c 100644 --- a/sdk/communication/azure-communication-identity/samples/identity_samples.py +++ b/sdk/communication/azure-communication-identity/samples/identity_samples.py @@ -19,12 +19,11 @@ 3) AZURE_CLIENT_SECRET - the secret of your active directory application 4) AZURE_TENANT_ID - the tenant ID of your active directory application 5) COMMUNICATION_M365_APP_ID - the application id of Microsoft 365 - 6) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of Microsoft 365 + 6) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of Microsoft 365 7) COMMUNICATION_M365_AAD_TENANT - the tenant ID of Microsoft 365 application 8) COMMUNICATION_MSAL_USERNAME - the username for authenticating via MSAL library 9) COMMUNICATION_MSAL_PASSWORD - the password for authenticating via MSAL library """ - from datetime import timedelta import os from azure.communication.identity._shared.utils import parse_connection_str diff --git a/sdk/communication/azure-communication-identity/samples/identity_samples_async.py b/sdk/communication/azure-communication-identity/samples/identity_samples_async.py index 80aa5fce4cd8..b7f74a8f2c7b 100644 --- a/sdk/communication/azure-communication-identity/samples/identity_samples_async.py +++ b/sdk/communication/azure-communication-identity/samples/identity_samples_async.py @@ -20,12 +20,11 @@ 4) AZURE_CLIENT_SECRET - the secret of your active directory application 5) AZURE_TENANT_ID - the tenant ID of your active directory application 6) COMMUNICATION_M365_APP_ID - the application id of Microsoft 365 - 7) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of M365 + 7) COMMUNICATION_M365_AAD_AUTHORITY - the Azure AD authority of M365 8) COMMUNICATION_M365_AAD_TENANT - the tenant ID of Microsoft 365 application 9) COMMUNICATION_MSAL_USERNAME - the username for authenticating via the MSAL library 10) COMMUNICATION_MSAL_PASSWORD - the password for authenticating via the MSAL library """ - from datetime import timedelta from azure.communication.identity._shared.utils import parse_connection_str from msal import PublicClientApplication diff --git a/sdk/communication/azure-communication-identity/sdk_packaging.toml b/sdk/communication/azure-communication-identity/sdk_packaging.toml new file mode 100644 index 000000000000..88dbadd37a99 --- /dev/null +++ b/sdk/communication/azure-communication-identity/sdk_packaging.toml @@ -0,0 +1,7 @@ +[packaging] +auto_update = false +package_name = "azure-communication-identity" +package_pprint_name = "Communication Identity Service" +package_doc_id = "" +is_stable = false +is_arm = false \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py b/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py index e552ff83a69a..2502a4713a88 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/communication_service_preparer.py @@ -1,4 +1,3 @@ -# pylint: disable=line-too-long,useless-suppression # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. diff --git a/sdk/communication/azure-communication-identity/tests/conftest.py b/sdk/communication/azure-communication-identity/tests/conftest.py index c0c11bca8276..3e8e7cbfbbae 100644 --- a/sdk/communication/azure-communication-identity/tests/conftest.py +++ b/sdk/communication/azure-communication-identity/tests/conftest.py @@ -33,7 +33,6 @@ test_proxy, add_general_regex_sanitizer, add_header_regex_sanitizer, - add_uri_regex_sanitizer, set_default_session_settings, add_body_key_sanitizer, add_oauth_response_sanitizer, @@ -41,29 +40,6 @@ ) from azure.communication.identity._shared.utils import parse_connection_str -# The recorded sessions were captured against api-version 2023-10-01. This package now -# defaults to 2026-09-23, so the request URI no longer matches the recording and every -# recorded test fails with "Unable to find a record for the request". -# -# The two versions are functionally identical for these operations, so the recordings remain -# valid; only the query string differs. Rewriting the request back to the recorded value lets -# them match. This is deliberately narrow -- it maps one exact version to one other exact -# version, so any other api-version still fails to match. -# -# Do NOT replace this with `ignored_query_parameters`: that substitutes a "Volatile" literal -# on both sides, so *any* api-version would match *any* recording, removing all coverage of -# the single field this release changes. -# -# Known limitation: sanitizers apply to both the recording and the incoming request, so the -# two versions become mutually interchangeable and a regression back to 2023-10-01 would not -# be caught here. tests/test_wire_contract.py asserts the exact api-version on the wire -# against a literal, which covers that gap. -# -# This retires itself: once the cassettes are re-recorded at 2026-09-23 the regex matches -# nothing and the sanitizer becomes inert. -RECORDED_API_VERSION = "2023-10-01" -CURRENT_API_VERSION = "2026-09-23" - @pytest.fixture(scope="session", autouse=True) def add_sanitizers(test_proxy): @@ -77,11 +53,6 @@ def add_sanitizers(test_proxy): add_oauth_response_sanitizer() - add_uri_regex_sanitizer( - regex=f"api-version={CURRENT_API_VERSION}", - value=f"api-version={RECORDED_API_VERSION}", - ) - connection_str = os.environ.get("COMMUNICATION_LIVETEST_DYNAMIC_CONNECTION_STRING") if connection_str is not None: endpoint, _ = parse_connection_str(connection_str) diff --git a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py index c13cafb32f00..6fdda359342e 100644 --- a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py +++ b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy.py @@ -10,7 +10,9 @@ from azure.core.pipeline import PipelineRequest from azure.core.pipeline.transport import HttpRequest from unittest.mock import MagicMock -from azure.communication.identity._shared.entra_token_guard_policy import EntraTokenGuardPolicy +from azure.communication.identity._shared.entra_token_guard_policy import ( + EntraTokenGuardPolicy +) class DummyResponse: @@ -18,7 +20,8 @@ def __init__(self, status_code=200, expires_on=None): self.http_response = MagicMock() self.http_response.status_code = status_code self.http_response.text.return_value = ( - '{"accessToken": {"expiresOn": "%s"}}' % expires_on if expires_on else "{}" + '{"accessToken": {"expiresOn": "%s"}}' % expires_on + if expires_on else '{}' ) @@ -30,7 +33,8 @@ def setUp(self): def test_send_cache_miss(self): self.policy.next.send.return_value = DummyResponse( - status_code=200, expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() + status_code=200, + expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() ) self.request.http_request.headers["Authorization"] = "token" response = self.policy.send(self.request) @@ -41,7 +45,8 @@ def test_send_cache_hit(self): # Simulate a valid cache self.policy._entra_token_cache = "token" self.policy._response_cache = DummyResponse( - status_code=200, expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() + status_code=200, + expires_on=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() ) self.request.http_request.headers["Authorization"] = "token" response = self.policy.send(self.request) diff --git a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py index 076870e4cc3c..1e1b452c746e 100644 --- a/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_entra_token_guard_policy_async.py @@ -21,7 +21,7 @@ def __init__(self, status_code=200, expires_on=None): def text(self): if self._expires_on: return '{"accessToken": {"expiresOn": "%s"}}' % self._expires_on - return "{}" + return '{}' class DummyPipelineResponse: @@ -66,4 +66,4 @@ async def test_send_invalid_response_raises(self): request = DummyRequest("token1") with pytest.raises(RuntimeError): - await policy.send(request) + await policy.send(request) \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py b/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py index 413abb22a6d6..c259bcda6ca8 100644 --- a/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py +++ b/sdk/communication/azure-communication-identity/tests/test_identifier_raw_id.py @@ -9,7 +9,6 @@ import unittest from azure.communication.identity import * - class IdentifierRawIdTest(unittest.TestCase): def test_raw_id(self): _assert_raw_id( @@ -99,42 +98,42 @@ def test_raw_id(self): ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="PUBLIC", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="PUBLIC" ), "8:acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="DOD", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="DOD" ), "8:dod-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="GCCH", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="GCCH" ), "8:gcch-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", ) _assert_raw_id( TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="PUBLIC", - raw_id="8:extension:legacyFormat", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="PUBLIC", + raw_id="8:extension:legacyFormat" ), "8:extension:legacyFormat", ) - + def test_identifier_from_raw_id(self): _assert_communication_identifier( "8:acs:bbbcbc1e-9f06-482a-b5d8-20e3f26ef0cd_45ab2481-1c1c-4005-be24-0ffb879b1130", @@ -214,64 +213,70 @@ def test_identifier_from_raw_id(self): MicrosoftTeamsAppIdentifier(app_id="45ab2481-1c1c-4005-be24-0ffb879b1130", cloud="GCCH"), ) _assert_phonenumber_identifier( - "4:+112345556789", PhoneNumberIdentifier(value="+112345556789"), withIsAnonymous=False, withAssertedId=False - ) + "4:+112345556789", + PhoneNumberIdentifier(value="+112345556789"), + withIsAnonymous=False, + withAssertedId=False) _assert_phonenumber_identifier( - "4:112345556789", PhoneNumberIdentifier(value="112345556789"), withIsAnonymous=False, withAssertedId=False - ) + "4:112345556789", + PhoneNumberIdentifier(value="112345556789"), + withIsAnonymous=False, + withAssertedId=False) _assert_phonenumber_identifier( - "4:otherFormat", PhoneNumberIdentifier(value="otherFormat"), withIsAnonymous=False, withAssertedId=False - ) + "4:otherFormat", + PhoneNumberIdentifier(value="otherFormat"), + withIsAnonymous=False, + withAssertedId=False) _assert_phonenumber_identifier( "4:207ffef6-9444-41fb-92ab-20eacaae2768", PhoneNumberIdentifier(value="207ffef6-9444-41fb-92ab-20eacaae2768"), withIsAnonymous=False, - withAssertedId=False, + withAssertedId=False ) # cspell:disable _assert_phonenumber_identifier( "4:207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", - PhoneNumberIdentifier( - value="207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", - raw_id="4:207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", - ), + PhoneNumberIdentifier(value="207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768", + raw_id="4:207ffef6-9444-41fb-92ab-20eacaae2768_207ffef6-9444-41fb-92ab-20eacaae2768"), withIsAnonymous=False, - withAssertedId=True, + withAssertedId=True ) _assert_phonenumber_identifier( "4:anonymous", PhoneNumberIdentifier(value="anonymous", raw_id="4:anonymous"), - withIsAnonymous=True, - withAssertedId=False, + withIsAnonymous=True, withAssertedId=False ) _assert_phonenumber_identifier( - "4:+112345556789", PhoneNumberIdentifier(value="+112345556789"), withIsAnonymous=False, withAssertedId=False + "4:+112345556789", + PhoneNumberIdentifier(value="+112345556789"), + withIsAnonymous=False, + withAssertedId=False ) _assert_communication_identifier( "8:acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="PUBLIC", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="PUBLIC", ), ) _assert_communication_identifier( "8:dod-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="DOD", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="DOD", ), ) _assert_communication_identifier( "8:gcch-acs:resource123_tenant123_45ab2481-1c1c-4005-be24-0ffb879b1130", TeamsExtensionUserIdentifier( - user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", - tenant_id="tenant123", - resource_id="resource123", - cloud="GCCH", + user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", + tenant_id="tenant123", + resource_id="resource123", + cloud="GCCH", ), ) # cspell:enable @@ -419,36 +424,36 @@ def test_equality_based_on_raw_id(self): user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) == TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="DOD", + cloud="DOD" ) == TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="DOD", + cloud="DOD" ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="GCCH", + cloud="GCCH" ) == TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="GCCH", + cloud="GCCH" ) # TeamsExtensionUserIdentifiers are not equal. @@ -456,50 +461,50 @@ def test_equality_based_on_raw_id(self): user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) != TeamsExtensionUserIdentifier( user_id="55ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) != TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant456", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) != TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource456", - cloud="PUBLIC", + cloud="PUBLIC" ) assert TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="PUBLIC", + cloud="PUBLIC" ) != TeamsExtensionUserIdentifier( user_id="45ab2481-1c1c-4005-be24-0ffb879b1130", tenant_id="tenant123", resource_id="resource123", - cloud="DOD", + cloud="DOD" ) - + # UnknownIdentifiers are equal. assert UnknownIdentifier(identifier="28:ag08-global:01234567-89ab-cdef-0123-456789abcdef") == UnknownIdentifier( identifier="28:ag08-global:01234567-89ab-cdef-0123-456789abcdef" @@ -526,7 +531,6 @@ def _assert_communication_identifier(raw_id, want): assert key in got.properties assert got.properties[key] == want.properties[key] - def _assert_phonenumber_identifier(raw_id, want, withIsAnonymous=False, withAssertedId=False): # type: (str, PhoneNumberIdentifier, Optional[dict]) -> None got = identifier_from_raw_id(raw_id) diff --git a/sdk/communication/azure-communication-identity/tests/test_token_exchange.py b/sdk/communication/azure-communication-identity/tests/test_token_exchange.py index 4917a79d8b66..2a5f036e8559 100644 --- a/sdk/communication/azure-communication-identity/tests/test_token_exchange.py +++ b/sdk/communication/azure-communication-identity/tests/test_token_exchange.py @@ -9,7 +9,9 @@ from azure.core.credentials import AccessToken from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from unittest.mock import MagicMock -from azure.communication.identity._shared.token_exchange import TokenExchangeClient +from azure.communication.identity._shared.token_exchange import ( + TokenExchangeClient +) class DummyPipelineResponse: @@ -21,8 +23,7 @@ def __init__(self, status_code=200, token="tok", expires_on="2999-12-31T23:59:59 else: self.http_response.text.return_value = ( f'{{"accessToken": {{"token": "{token}", "expiresOn": "{expires_on}"}}}}' - if status_code == 200 - else "{}" + if status_code == 200 else '{}' ) diff --git a/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py b/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py index adb5b5c4df3c..25a67799c075 100644 --- a/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_token_exchange_async.py @@ -10,7 +10,9 @@ from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from unittest.mock import MagicMock, AsyncMock -from azure.communication.identity._shared.token_exchange_async import TokenExchangeClient +from azure.communication.identity._shared.token_exchange_async import ( + TokenExchangeClient +) class DummyPipelineResponse: @@ -22,8 +24,7 @@ def __init__(self, status_code=200, token="tok", expires_on="2999-12-31T23:59:59 else: self.http_response.text.return_value = ( f'{{"accessToken": {{"token": "{token}", "expiresOn": "{expires_on}"}}}}' - if status_code == 200 - else "{}" + if status_code == 200 else '{}' ) diff --git a/sdk/communication/azure-communication-identity/tests/test_token_utils.py b/sdk/communication/azure-communication-identity/tests/test_token_utils.py index c2eceb6258bb..0857dc5961e4 100644 --- a/sdk/communication/azure-communication-identity/tests/test_token_utils.py +++ b/sdk/communication/azure-communication-identity/tests/test_token_utils.py @@ -28,7 +28,8 @@ def __init__(self, status_code=200, expires_on=None): self.http_response = MagicMock() self.http_response.status_code = status_code self.http_response.text.return_value = ( - '{"accessToken": {"expiresOn": "%s"}}' % expires_on if expires_on else "{}" + '{"accessToken": {"expiresOn": "%s"}}' % expires_on + if expires_on else '{}' ) @@ -70,20 +71,21 @@ def test_create_request_message_and_uri(self): assert uri.startswith("https://endpoint") assert TEAMS_EXTENSION_ENDPOINT in uri - req = utils.create_request_message("https://endpoint", [TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) + req = utils.create_request_message("https://endpoint", + [TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) assert req.method == "POST" assert req.headers["Accept"] == "application/json" assert req.headers["Content-Type"] == "application/json" def test_determine_endpoint_and_api_version_teams_extension(self): - endpoint, api_version = utils.determine_endpoint_and_api_version([TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) + endpoint, api_version = utils.determine_endpoint_and_api_version( + [TEAMS_EXTENSION_SCOPE_PREFIX + ".default"]) assert endpoint == TEAMS_EXTENSION_ENDPOINT assert api_version == TEAMS_EXTENSION_API_VERSION def test_determine_endpoint_and_api_version_communication_clients(self): endpoint, api_version = utils.determine_endpoint_and_api_version( - [COMMUNICATION_CLIENTS_SCOPE_PREFIX + ".default"] - ) + [COMMUNICATION_CLIENTS_SCOPE_PREFIX + ".default"]) assert endpoint == COMMUNICATION_CLIENTS_ENDPOINT assert api_version == COMMUNICATION_CLIENTS_API_VERSION diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index c74d225ceef0..9598cecbe71d 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -39,7 +39,7 @@ def __init__(self, resource_endpoint, token_credential, scopes): def exchange_entra_token(self): return DummyToken("dummy", 9999999999) - + class DummyTokenExchangeClientSwitch: def __init__(self, resource_endpoint, token_credential, scopes): @@ -251,11 +251,11 @@ def test_missing_fields_raises_value_error(self): def test_all_fields_present_calls_token_exchange(monkeypatch): # Patch TokenExchangeClient to our dummy - with patch( - "azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient - ): + with patch("azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] + resource_endpoint="https://endpoint", + token_credential=MagicMock(), + scopes=["scope"] ) token = cred.get_token() assert token.token == "dummy" @@ -263,10 +263,11 @@ def test_all_fields_present_calls_token_exchange(monkeypatch): def test_missing_scopes_calls_token_exchange(monkeypatch): # Patch TokenExchangeClient to our dummy - with patch( - "azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient - ): - cred = CommunicationTokenCredential(resource_endpoint="https://endpoint", token_credential=MagicMock()) + with patch("azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClient): + cred = CommunicationTokenCredential( + resource_endpoint="https://endpoint", + token_credential=MagicMock() + ) token = cred.get_token() assert token.token == "dummy" assert token.expires_on == 9999999999 @@ -274,11 +275,11 @@ def test_missing_scopes_calls_token_exchange(monkeypatch): def test_token_exchange_refreshes_from_expired_to_valid(monkeypatch): # Patch TokenExchangeClient to DummyTokenExchangeClientSwitch # First call returns expired token - when initializing the token - with patch( - "azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClientSwitch - ): + with patch("azure.communication.identity._shared.user_credential.TokenExchangeClient", DummyTokenExchangeClientSwitch): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] + resource_endpoint="https://endpoint", + token_credential=MagicMock(), + scopes=["scope"] ) # Second call will trigger a refresh and generate a valid token token2 = cred.get_token() diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 3713158f3d4b..80d33af1840c 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -23,13 +23,11 @@ from azure.communication.identity._shared.utils import get_current_utc_as_int from _shared.helper import generate_token_with_custom_expiry - class DummyAsyncToken: def __init__(self, token, expires_on): self.token = token self.expires_on = expires_on - class DummyAsyncTokenExchangeClient: def __init__(self, resource_endpoint, token_credential, scopes): self.resource_endpoint = resource_endpoint @@ -39,7 +37,6 @@ def __init__(self, resource_endpoint, token_credential, scopes): async def exchange_entra_token(self): return DummyAsyncToken("dummy", 9999999999) - class DummyAsyncTokenExchangeClientSwitch: def __init__(self, resource_endpoint, token_credential, scopes): self.resource_endpoint = resource_endpoint @@ -286,11 +283,13 @@ async def test_missing_fields_raises_value_error(self): @pytest.mark.asyncio async def test_all_fields_present_calls_token_exchange_async(self): with patch( - "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", - DummyAsyncTokenExchangeClient, + "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", + DummyAsyncTokenExchangeClient, ): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] + resource_endpoint="https://endpoint", + token_credential=MagicMock(), + scopes=["scope"] ) token = await cred.get_token() assert token.token == "dummy" @@ -299,10 +298,13 @@ async def test_all_fields_present_calls_token_exchange_async(self): @pytest.mark.asyncio async def test_missing_scopes_calls_token_exchange_async(self): with patch( - "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", - DummyAsyncTokenExchangeClient, + "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", + DummyAsyncTokenExchangeClient, ): - cred = CommunicationTokenCredential(resource_endpoint="https://endpoint", token_credential=MagicMock()) + cred = CommunicationTokenCredential( + resource_endpoint="https://endpoint", + token_credential=MagicMock() + ) token = await cred.get_token() assert token.token == "dummy" assert token.expires_on == 9999999999 @@ -311,11 +313,13 @@ async def test_missing_scopes_calls_token_exchange_async(self): async def test_token_exchange_refreshes_from_expired_to_valid_async(self): # First call returns expired token on initialization of _token with patch( - "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", - DummyAsyncTokenExchangeClientSwitch, + "azure.communication.identity._shared.user_credential_async.AsyncTokenExchangeClient", + DummyAsyncTokenExchangeClientSwitch, ): cred = CommunicationTokenCredential( - resource_endpoint="https://endpoint", token_credential=MagicMock(), scopes=["scope"] + resource_endpoint="https://endpoint", + token_credential=MagicMock(), + scopes=["scope"] ) # Second call returns valid token diff --git a/sdk/communication/azure-communication-identity/tests/test_utils.py b/sdk/communication/azure-communication-identity/tests/test_utils.py index 0c875abbbcbc..3e648881f5e9 100644 --- a/sdk/communication/azure-communication-identity/tests/test_utils.py +++ b/sdk/communication/azure-communication-identity/tests/test_utils.py @@ -27,15 +27,21 @@ def test_convert_datetime_to_utc_int(self): assert utc_naive_time_in_sec == 0 # PST is UTC-8 (Vancouver/Pacific Time) pst_tz = timezone(timedelta(hours=-8)) # PST is UTC-8 - pst_time_in_sec = _convert_datetime_to_utc_int(datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pst_tz)) + pst_time_in_sec = _convert_datetime_to_utc_int( + datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pst_tz) + ) assert pst_time_in_sec == 8 * 3600 # EST is UTC-5 (Eastern Time) est_tz = timezone(timedelta(hours=-5)) # EST is UTC-5 - est_time_in_sec = _convert_datetime_to_utc_int(datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=est_tz)) + est_time_in_sec = _convert_datetime_to_utc_int( + datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=est_tz) + ) assert est_time_in_sec == 5 * 3600 # CST is UTC+8 (China Standard Time) cst_tz = timezone(timedelta(hours=8)) # CST is UTC+8 - cst_time_in_sec = _convert_datetime_to_utc_int(datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=cst_tz)) + cst_time_in_sec = _convert_datetime_to_utc_int( + datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=cst_tz) + ) assert cst_time_in_sec == -8 * 3600 def test_access_token_expiry_deserialized_correctly_from_payload(self): diff --git a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py b/sdk/communication/azure-communication-identity/tests/test_wire_contract.py deleted file mode 100644 index d07284cebdf0..000000000000 --- a/sdk/communication/azure-communication-identity/tests/test_wire_contract.py +++ /dev/null @@ -1,388 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -"""Wire-contract tests for CommunicationIdentityClient. - -These assert what the client actually puts on the wire, rather than what the -convenience layer returns. They exist because the move from AutoRest to the -TypeSpec DPG emitter changed three things that are invisible to an API-surface -check and are not covered by the recorded tests: - -1. ``expiresInMinutes`` must be omitted when the caller supplies no expiry. - msrest dropped ``None`` fields during serialization; the generated code - forwards the request body as-is, so without a fix an explicit ``null`` - reaches a property the service constrains to [60, 1440]. - -2. ``AccessToken.expires_on`` must remain the raw service string. The TypeSpec - model declares ``expiresOn: utcDateTime``, so attribute access on the - generated model returns a ``datetime``; every published version of this SDK - exposed the string. - -3. The default api-version must be the one the package claims to target. - -Requests are captured at the transport, which is the last pipeline stage before -the network, so these are wire observations rather than serializer return -values. -""" - -import json -from datetime import timedelta - -import pytest -from azure.core.exceptions import HttpResponseError -from azure.core.pipeline.transport import HttpTransport - -from azure.communication.identity import ( - CommunicationIdentityClient, - CommunicationTokenScope, -) -from azure.communication.identity._api_versions import DEFAULT_VERSION -from azure.communication.identity._shared.models import CommunicationUserIdentifier - -WIRE_EXPIRY = "2026-09-23T12:00:00.1234567Z" -EXPECTED_API_VERSION = "2026-09-23" -FAKE_ENDPOINT = "https://sanitized.communication.azure.com" -FAKE_KEY = "a2V5" * 8 - - -class _FakeResponse: - def __init__(self, request, status_code, body): - self.request = request - self.status_code = status_code - self._body = json.dumps(body).encode() if body is not None else b"" - self.headers = {"content-type": "application/json"} - self.reason = "OK" - self.content_type = "application/json" - self.is_closed = True - self.is_stream_consumed = True - - @property - def content(self): - return self._body - - def text(self, encoding=None): - return self._body.decode() - - def json(self): - return json.loads(self._body) - - def read(self): - return self._body - - def close(self): - pass - - def raise_for_status(self): - pass - - -class _RawResponse: - """Returns a body verbatim, so non-JSON and malformed payloads can be exercised.""" - - def __init__(self, request, status_code, raw_body, content_type="application/json"): - self.request = request - self.status_code = status_code - self._body = raw_body - self.headers = {"content-type": content_type} - self.reason = "Error" - self.content_type = content_type - self.is_closed = True - self.is_stream_consumed = True - - @property - def content(self): - return self._body - - def text(self, encoding=None): - return self._body.decode("utf-8", "replace") - - def json(self): - return json.loads(self._body) - - def read(self): - return self._body - - def close(self): - pass - - def raise_for_status(self): - pass - - -class _ErrorTransport(HttpTransport): - """Replies to every request with one canned error response.""" - - def __init__(self, status_code, raw_body, content_type="application/json"): - self.status_code = status_code - self.raw_body = raw_body - self.content_type = content_type - - def send(self, request, **kwargs): - return _RawResponse(request, self.status_code, self.raw_body, self.content_type) - - def open(self): - pass - - def close(self): - pass - - def __enter__(self): - return self - - def __exit__(self, *args): - pass - - -class _CapturingTransport(HttpTransport): - """Records each outgoing request and replies with a canned response.""" - - def __init__(self): - self.requests = [] - - def send(self, request, **kwargs): - self.requests.append(request) - token = {"token": "jwt.tok.sig", "expiresOn": WIRE_EXPIRY} - path = request.url.split("?")[0] - if path.endswith("/identities"): - return _FakeResponse(request, 201, {"identity": {"id": "8:acs:u"}, "accessToken": token}) - if path.endswith(":issueAccessToken") or path.endswith("teamsUser/:exchangeAccessToken"): - return _FakeResponse(request, 200, token) - return _FakeResponse(request, 204, None) - - def open(self): - pass - - def close(self): - pass - - def __enter__(self): - return self - - def __exit__(self, *args): - pass - - -@pytest.fixture -def transport(): - return _CapturingTransport() - - -@pytest.fixture -def client(transport): - return CommunicationIdentityClient(FAKE_ENDPOINT, FAKE_KEY, transport=transport) - - -def _sent_body(transport): - return json.loads(transport.requests[-1].content) - - -class TestRequestBody: - """expiresInMinutes must be absent unless the caller asked for an expiry.""" - - def test_create_user_and_token_omits_expiry_when_not_requested(self, client, transport): - client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) - body = _sent_body(transport) - assert "expiresInMinutes" not in body - assert body["createTokenWithScopes"] == ["chat"] - - def test_create_user_and_token_sends_expiry_when_requested(self, client, transport): - client.create_user_and_token( - scopes=[CommunicationTokenScope.CHAT], - token_expires_in=timedelta(hours=2), - ) - assert _sent_body(transport)["expiresInMinutes"] == 120 - - def test_get_token_omits_expiry_when_not_requested(self, client, transport): - client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.VOIP]) - body = _sent_body(transport) - assert "expiresInMinutes" not in body - assert body["scopes"] == ["voip"] - - def test_get_token_sends_expiry_when_requested(self, client, transport): - client.get_token( - CommunicationUserIdentifier("8:acs:u"), - scopes=[CommunicationTokenScope.VOIP], - token_expires_in=timedelta(hours=3), - ) - assert _sent_body(transport)["expiresInMinutes"] == 180 - - def test_none_scopes_are_omitted_not_sent_as_null(self, client, transport): - """A None scope list must be dropped, as msrest dropped it. - - Passing None for a required argument is a caller error, but it has to fail the way it - always failed. The AutoRest client sent {} because msrest omitted None fields; sending - {"scopes": null} instead is a different request for a proxy, gateway or log to observe. - This does not assert an exception: the previous client did not raise here either. - """ - try: - client.get_token(CommunicationUserIdentifier("8:acs:u"), None) - except Exception: # pylint: disable=broad-except - pass - assert _sent_body(transport) == {} - - def test_none_create_scopes_are_omitted_not_sent_as_null(self, client, transport): - try: - client.create_user_and_token(None) - except Exception: # pylint: disable=broad-except - pass - assert _sent_body(transport) == {} - - def test_empty_body_still_sends_json_content_type(self, client, transport): - """An empty JSON body must still be typed as JSON. - - The generated operation drops content_type whenever the body is falsy, and an empty - dict is falsy -- so without the policy azure-core falls back to text/plain. The - serialized body is the string "{}", which is truthy, so a body-presence check misses - this case entirely. - """ - try: - client.create_user_and_token(None) - except Exception: # pylint: disable=broad-except - pass - assert transport.requests[-1].headers["Content-Type"] == "application/json" - - def test_empty_scope_list_is_still_sent(self, client, transport): - """An empty list is a value, not an absence, and must survive.""" - try: - client.get_token(CommunicationUserIdentifier("8:acs:u"), []) - except Exception: # pylint: disable=broad-except - pass - assert _sent_body(transport) == {"scopes": []} - - -class TestAccessTokenExpiry: - """expires_on must be the untouched service string, not a deserialized datetime.""" - - def test_create_user_and_token_expiry_is_raw_string(self, client): - _, token = client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) - assert isinstance(token.expires_on, str) - assert token.expires_on == WIRE_EXPIRY - - def test_get_token_expiry_is_raw_string(self, client): - token = client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.CHAT]) - assert isinstance(token.expires_on, str) - assert token.expires_on == WIRE_EXPIRY - - def test_get_token_for_teams_user_expiry_is_raw_string(self, client): - token = client.get_token_for_teams_user("aad-token", "client-id", "user-object-id") - assert isinstance(token.expires_on, str) - assert token.expires_on == WIRE_EXPIRY - - -class TestRequestHeaders: - """Request headers must match what the AutoRest-generated client sent. - - The TypeSpec DPG emitter drops three headers the previous client sent: - ``Content-Type`` on the bodyless ``create_user`` POST, and ``Accept`` on - ``revoke_tokens`` and ``delete_user`` (both typed as 204-no-content, for - which the emitter produces no ``Accept``). - - The service is indifferent to these -- measured against a live resource -- - but a proxy, gateway or request log keying on headers would observe the - change, so they are restored in the convenience layer. - - Expected values are literals rather than references to the constants that - produce them: asserting against ``_ACCEPT_JSON`` would compare the wire to - the same value that set it and would pass even if both were wrong. - """ - - def test_create_user_sends_content_type(self, client, transport): - client.create_user() - assert transport.requests[-1].headers["Content-Type"] == "application/json" - - def test_create_user_sends_no_body_despite_content_type(self, client, transport): - """Content-Type must not cause a body to be sent -- AutoRest sent none.""" - client.create_user() - assert transport.requests[-1].content is None - - def test_revoke_tokens_sends_accept(self, client, transport): - client.revoke_tokens(CommunicationUserIdentifier("8:acs:u")) - assert transport.requests[-1].headers["Accept"] == "application/json" - - def test_delete_user_sends_accept(self, client, transport): - client.delete_user(CommunicationUserIdentifier("8:acs:u")) - assert transport.requests[-1].headers["Accept"] == "application/json" - - def test_body_bearing_operations_still_send_both_headers(self, client, transport): - client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) - headers = transport.requests[-1].headers - assert headers["Accept"] == "application/json" - assert headers["Content-Type"] == "application/json" - - def test_caller_supplied_content_type_is_not_overridden(self, client, transport): - """The policy must defer to a Content-Type already on the request.""" - client.create_user(headers={"Content-Type": "application/custom"}) - assert transport.requests[-1].headers["Content-Type"] == "application/custom" - - -class TestErrorResponses: - """A non-envelope error body must still surface the real HTTP status. - - Gateways, front doors, WAFs and throttling pages return HTML or non-envelope JSON on - failure, so this is triggered by infrastructure rather than by caller error. Customer - code branching on ``e.status_code`` (for example retrying on 429) depends on the status - surviving a body the deserializer cannot parse. - - The equivalent JavaScript client raises a bare ``TypeError`` with no status in these - cases. Python is correct because the generated code raises - ``HttpResponseError(response=response, model=error)`` -- taking the status from the - response rather than the parsed model -- and ``_failsafe_deserialize`` swallows the parse - failure. These tests pin that black-box guarantee so a future emitter change cannot - regress it silently. - - Assertions are deliberately limited to the exception type and status code. The internals - of ``_failsafe_deserialize`` are not asserted: they are the mechanism, not the contract. - """ - - @pytest.mark.parametrize( - "status_code,raw_body,content_type", - [ - pytest.param(502, b"Bad Gateway", "text/html", id="html_body"), - pytest.param(500, b'{"message":"oops"}', "application/json", id="non_envelope_json"), - pytest.param(503, b"", "application/json", id="empty_body"), - pytest.param(429, b"Too Many Requests", "text/html", id="throttling_html"), - ], - ) - def test_malformed_error_body_still_reports_status(self, status_code, raw_body, content_type): - client = CommunicationIdentityClient( - FAKE_ENDPOINT, FAKE_KEY, transport=_ErrorTransport(status_code, raw_body, content_type) - ) - with pytest.raises(HttpResponseError) as caught: - client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.CHAT]) - assert caught.value.status_code == status_code - - def test_well_formed_error_envelope_still_reports_status(self): - body = b'{"error":{"code":"Unauthorized","message":"denied"}}' - client = CommunicationIdentityClient(FAKE_ENDPOINT, FAKE_KEY, transport=_ErrorTransport(401, body)) - with pytest.raises(HttpResponseError) as caught: - client.get_token(CommunicationUserIdentifier("8:acs:u"), scopes=[CommunicationTokenScope.CHAT]) - assert caught.value.status_code == 401 - - -class TestApiVersion: - """Every request must carry the api-version the package targets. - - The expected version is asserted as a literal rather than by reading - ``DEFAULT_VERSION``. Comparing the wire against the same constant that - produced it is self-referential and cannot detect a wrong default -- that - weakness was found by mutation testing, where changing ``DEFAULT_VERSION`` - left the loop below passing. - """ - - def test_default_api_version_is_sent_on_every_operation(self, client, transport): - user = CommunicationUserIdentifier("8:acs:u") - client.create_user() - client.create_user_and_token(scopes=[CommunicationTokenScope.CHAT]) - client.get_token(user, scopes=[CommunicationTokenScope.CHAT]) - client.get_token_for_teams_user("aad-token", "client-id", "user-object-id") - client.revoke_tokens(user) - client.delete_user(user) - - assert len(transport.requests) == 6 - for request in transport.requests: - assert f"api-version={EXPECTED_API_VERSION}" in request.url - - def test_default_api_version_matches_shipped_value(self): - assert DEFAULT_VERSION.value == EXPECTED_API_VERSION diff --git a/sdk/communication/azure-communication-identity/tsp-location.yaml b/sdk/communication/azure-communication-identity/tsp-location.yaml deleted file mode 100644 index ffe6b6c58998..000000000000 --- a/sdk/communication/azure-communication-identity/tsp-location.yaml +++ /dev/null @@ -1,4 +0,0 @@ -directory: specification/communication/data-plane/Identity -commit: 1893171028aed5a757fc78c2d4aa439c14df4afc -repo: Azure/azure-rest-api-specs -additionalDirectories: From ee656544ded4ecec713eb1ed6ca04493d433b850 Mon Sep 17 00:00:00 2001 From: John Fraser Date: Thu, 10 Sep 2026 23:55:15 -0700 Subject: [PATCH 16/16] Update Communication Identity API version Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../azure/communication/identity/_api_versions.py | 3 ++- .../communication/identity/_communication_identity_client.py | 2 +- .../identity/aio/_communication_identity_client_async.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py index 5e6ef85d3a83..b45d464ccf30 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py @@ -12,6 +12,7 @@ class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): V2022_06_01 = "2022-06-01" V2022_10_01 = "2022-10-01" V2023_10_01 = "2023-10-01" + V2026_09_23 = "2026-09-23" -DEFAULT_VERSION = ApiVersion.V2023_10_01 +DEFAULT_VERSION = ApiVersion.V2026_09_23 diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py index ea003e7c564e..758aaa88a3e0 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -31,7 +31,7 @@ class CommunicationIdentityClient(object): :param Union[TokenCredential, AzureKeyCredential] credential: The credential we use to authenticate against the service. :keyword api_version: Azure Communication Identity API version. - Default value is "2022-10-01". Note that overriding this default value may result in unsupported behavior. + Default value is "2026-09-23". Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str .. admonition:: Example: diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py index 466f314d6673..77878d0326df 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -30,7 +30,7 @@ class CommunicationIdentityClient: :param Union[AsyncTokenCredential, AzureKeyCredential] credential: The credential we use to authenticate against the service. :keyword api_version: Azure Communication Identity API version. - Default value is "2022-06-01". Note that overriding this default value may result in unsupported behavior. + Default value is "2026-09-23". Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str .. admonition:: Example: