Skip to content

Update Communication Identity API version to 2026-09-23 - #48973

Open
John Fraser (JohnRFraser) wants to merge 16 commits into
Azure:mainfrom
JohnRFraser:johnfraser/communication-identity-2026-09-23
Open

Update Communication Identity API version to 2026-09-23#48973
John Fraser (JohnRFraser) wants to merge 16 commits into
Azure:mainfrom
JohnRFraser:johnfraser/communication-identity-2026-09-23

Conversation

@JohnRFraser

Copy link
Copy Markdown
Member

Description

Update the Communication Identity client API-version enum and default to 2026-09-23. The sync and async client documentation now reports the same default.

This is an API-version-only change and does not regenerate the client.

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

Validation: 117 code-only tests passed. Playback tests could not run because this checkout does not contain the required recordings.

…onfig.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.
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>
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>
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>
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>
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>
_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>
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>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
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>
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 7b1196e:

                              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>
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>
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 7b1196e - 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>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Add the CHANGELOG entry and focused API-version tests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates the Communication Identity API version enum, default, and sync/async documentation to 2026-09-23.

Changes:

  • Adds ApiVersion.V2026_09_23 as the default.
  • Aligns sync and async client documentation.
File summaries
File Changes and findings
sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py Updates async client documentation.
sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py Updates sync client documentation.
sdk/communication/azure-communication-identity/azure/communication/identity/_api_versions.py Adds and selects the new API version. Nit (3 votes): Add an unreleased CHANGELOG entry. Moderate (2 votes): Add focused sync/async coverage for the default and override path.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.



DEFAULT_VERSION = ApiVersion.V2023_10_01
DEFAULT_VERSION = ApiVersion.V2026_09_23


DEFAULT_VERSION = ApiVersion.V2023_10_01
DEFAULT_VERSION = ApiVersion.V2026_09_23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants