You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Health Deid] Fix CI lint via regeneration, surface live test job errors, add test-resources readiness probe - #49627
This PR combines three azure-health-deidentification fixes, plus a temporary shim for a regenerated-runtime regression. It supersedes #49625 and #49626.
1. Fix CI: stale generated code and regeneration
Problem
The python - healthdataaiservices - ci pipeline has failed on every run since July:
pyrightreportArgumentType: issubclass receives type | None in _serialization.py:793 and _utils/serialization.py:793.
pylint C0123 unidiomatic-typecheck in _model_base.py and _utils/model_base.py, at lines 821, 824 and 830.
_serialization.py, _model_base.py, _vendor.py and aio/_vendor.py are leftovers from an older emitter. They only reference each other. The _utils/ copies were also out of date.
Fix
Delete the four stale files.
Regenerate from TypeSpec with the repo-pinned @azure-tools/typespec-python 0.63.10. The current emitter output passes pyright and pylint without any suppressions.
Regeneration side effects (please review)
tsp-location.yaml: the spec moved to specification/healthdataaiservices/data-plane/HealthDataAIServices. It now points at 6de384008f55f42ea97ee0654f237c74fb05ee89, the merge commit of [HealthDataAIServices] Disable Python TypedDict generation for DeID data-plane SDK azure-rest-api-specs#47496 (see the TypedDict item below). Within this spec directory, the only change since the previously pinned c7d9e3379dd76fe55ce476f7aa312a4fb938615c is the generate-typeddict option in tspconfig.yaml. The old pinned commit no longer compiles with the current typespec-azure-core (@useDependency(Azure.Core.Versions.v1_0_Preview_2)). I diffed the TypeSpec, and the API surface is unchanged. The only differences are lint #suppress lines, removal of the Azure.Core.Versions dependency, main.tsp importing client.tsp, and tspconfig output paths.
setup.py → pyproject.toml: the emitter now generates PEP 621 packaging.
Minimum Python is now 3.10 (was 3.9), and the minimum azure-core is now 1.37.0 (was 1.30.0). README is updated.
DELETE requests no longer send Accept: application/json: delete_job returns 204 with no body.
Test matcher: tests/conftest.py adds set_custom_default_matcher(excluded_headers="Accept") so the existing recordings, which include Accept on DELETE, still match. The alternative is re-recording.
polling=False in the sync test_create_cancel_delete: once Accept is excluded, the background LRO poller raced the test's own get_job calls for recorded GET responses. It failed about 3 of 5 runs. The test already polls the job status itself.
CHANGELOG: new 1.1.0b2 (Unreleased) entry under Other Changes, and _version.py is bumped to 1.1.0b2.
2. Surface job errors in live test failures; random live job names (from #49626)
Problem
Live runs of test_create_wait_finish, test_create_wait_finish_async, test_list_job_docs_pagination and deidentify_documents_async.py kept failing with HttpResponseError: Operation returned an invalid status 'OK'. That message is how azure-core LRO polling reports a job that reached Failed. It hid the actual job and document errors, so the underlying service issue went undiagnosed for weeks.
Fixed live job names: conftest only regenerated the uniquifier when recording. Live CI sets AZURE_SKIP_LIVE_RECORDING=True, so every live run reused the checked-in uniquifier.conf value. Any live runs that share resources would then try to create jobs with the same names.
Running after result(timeout=120):LROPoller.result(timeout) returns the current resource once the timeout expires. This is not an SDK bug. The tests just never checked whether the poller had finished.
Fix
Test helper:tests/deid_base_test_case.py adds wait_for_job / wait_for_job_async.
They wait up to 600s and fail explicitly if the job hasn't finished.
On failure they fetch the job and report its status, job.error, summary counts, and up to 20 non-succeeded documents with their errors.
Fetching diagnostics never hides the original exception. If listing documents fails partway, the job details and the documents already collected are kept, and the listing error is appended.
Batch tests: the three batch tests use the helper. Their per-document asserts now include the document location and error.
tests/conftest.py: every live run gets a random uniquifier; AZURE_TEST_KEEP_UNIQUIFIER opts out. It is written to uniquifier.conf only when recording, so playback stays deterministic.
Samples:deidentify_documents and list_job_documents, sync and async, print Job <name> ended with status <status>: <error> and re-raise on failure. The async samples also close their credential. The README snippet is regenerated from samples/deidentify_documents.py with azpysdk update_snippet.
3. Readiness probe in test-resources-post.ps1 (from #49625)
Problem
The post-deployment script opens the storage network ACL, then waits a fixed 30 seconds before uploading the test data. The role assignments and the ACL change can take longer to take effect, so live deployments sometimes fail with 403 AuthorizationFailure. The storage cmdlets also didn't stop the script on error, so a failed upload could be missed until tests ran.
Fix
Added an Invoke-WithRetry helper with exponential backoff: it starts at 5s, doubles, caps at 60s, and gives up after about 5 minutes. When it gives up, it throws an error that names the operation and the last error.
Get-AzStorageContainer (used as a readiness check) and each Set-AzStorageBlobContent upload run inside the helper with -ErrorAction Stop. Set-AzStorageAccount also uses -ErrorAction Stop.
The fixed sleep is replaced by a 30-second minimum wait, measured from the network rule update. A successful upload proves the test principal's RBAC and the network rule are in effect, but not the DeID service managed identity's role assignment. The minimum wait gives that assignment time to propagate.
Uploads use Get-ChildItem -File, so only files are uploaded, never directories.
4. Temporary shim: keep model list fields bound to one object
Problem
The regenerated _utils/model_base.py caches mutable fields when they are read as properties: _RestField.__get__ stores a newly deserialized list/dict/set back into the model. Any list previously returned by model["field"] is then detached. For example, filters = source["extensions"]; source.extensions; filters.append(".csv") dropped .csv on serialization, which worked in 1.1.0b1. The same emitter change fixed property-first mutations (source.extensions.append(...)), which were lost in 1.1.0b1. No released emitter fixes the dict-then-property case; the template on microsoft/typespec main is unchanged.
Fix
patch_sdk() in azure/health/deidentification/_patch.py wraps the generated _RestField.__get__. If the generated getter replaced the container, the shim copies the new contents into the original object and keeps that one in the model. The cache flag and setter behavior are unchanged, and the wrap is applied once (the aio package shares _utils and imports the root package first).
tests/test_model_container_identity.py (unit tests, no recordings) covers all five dict/property access orders for SourceStorageLocation.extensions (list of str) and TaggedPhiEntities.entities (list of models, built from kwargs and from a raw mapping), and checks the serialized output.
On the combined branch, run locally with the repo tooling (azpysdk, pyright 1.1.407, the same as CI):
azpysdk black: clean
azpysdk pyright: pass
azpysdk pylint: 10.00/10
azpysdk mypy: pass
azpysdk update_snippet: README is in sync
pytest tests in playback: 32/32 passed in each of 3 runs (13 recorded tests plus 19 new model unit tests). The new tests were confirmed to fail with the shim disabled.
test-resources-post.ps1: no PowerShell parse errors
Earlier validation, done on the separate branches before they were combined:
The retry helper and the whole post script were tested with the Az cmdlets and Start-Sleep mocked. This covered success after retries, the time-budget error, the 60s cap, and the final minimum wait.
One live deployment per branch with New-TestResources.ps1, using the user's own (non-CI) credentials, torn down with Remove-TestResources.ps1 afterwards.
The test-resources branch: the post script succeeded on the first attempt. This run was before the 30-second minimum wait was added.
The diagnostics branch: 13/13 live tests and the 4 changed samples passed. Some tests needed a rerun after transient local Azure CLI credential failures.
Limitations
The combined branch was not run live. The codegen and regeneration changes were validated in playback only.
I did not re-record; the Accept matcher exclusion is used instead.
The retry path was only exercised with mocks, because the live run hit no propagation delay. Nothing here has run in the CI live pipeline with the test service principal.
The samples still call result(timeout=60/120), matching the README.
Local setup workarounds that are not in this PR:
Generation ran through a short subst drive, because Windows MAX_PATH breaks the emitter's internal black step.
Fixes pyright reportArgumentType (issubclass) and pylint C0123 failures in
the healthdataaiservices CI pipeline.
- Delete stale _serialization.py, _model_base.py, _vendor.py, aio/_vendor.py
left over from an older emitter (only referenced each other).
- Point tsp-location.yaml at the relocated spec
(specification/healthdataaiservices/data-plane/HealthDataAIServices @ c7d9e33)
and regenerate with @azure-tools/typespec-python 0.63.10. No API changes.
- Emitter moves packaging to pyproject.toml (Python >=3.10, azure-core >=1.37.0)
and adds types.py; bump to 1.1.0b2 and note it in CHANGELOG/README.
- Tests: exclude Accept header from matching (regenerated delete_job no longer
sends it) and disable the background LRO poller in the sync create/cancel/delete
test, which otherwise races the test's own GETs in playback.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
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.
Regenerated with generate-typeddict: false so the JSON overloads match
1.1.0b1 and the inaccurate request-body TypedDicts (types.py) are removed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
…s-post.ps1
Role assignment and network ACL propagation can take minutes after
deployment, causing intermittent 403 AuthorizationFailure on upload.
Retry container access and each blob upload with exponential backoff
(5s initial, 60s cap, 5 min budget) and fail with a clear error.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0d9b0092-996f-4265-b2da-1effbf3945c6
The retry loop proves the test principal's RBAC and the network rule, but
not the DeID managed identity's role assignment. Restore the previous 30s
wait as a minimum measured from the network rule update; time spent on
retries and uploads counts toward it.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0d9b0092-996f-4265-b2da-1effbf3945c6
- Add wait_for_job/wait_for_job_async test helpers that report job status,
error, summary, and unsuccessful documents instead of azure-core's generic
'Operation returned an invalid status OK', and fail explicitly when the job
is not terminal after the timeout (now 600s).
- Generate a fresh uniquifier for every live run (including
AZURE_SKIP_LIVE_RECORDING); only persist it when recording.
- Samples print job.error before re-raising on job failure.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 40cdcc81-accc-4b2e-af24-fd79c6480740
Josiah Vinson (jovinson-ms)
changed the title
[Health Deidentification] Remove stale generated files and regenerate to fix pyright/pylint
[Health Deid] Fix CI lint via regeneration, surface live test job errors, add test-resources readiness probe
Oct 21, 2026
…edentials
The diagnostic get_job lookup in the samples' error handling could raise
(for example 404 when creation was rejected) and replace the original
error. It is now wrapped so the original exception is always re-raised.
The async samples close the credential via async with.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
In both describe_job and describe_job_async, if get_job succeeds but document enumeration fails—even on a later page—the outer catch discards the available job status/error and any collected document errors. The assertion then loses the service error this change is meant to expose. Catch document-listing failures separately and append that failure to the retained job and partial-document diagnostics.
Spec PR Azure/azure-rest-api-specs#47496 adds generate-typeddict: false to
the Python emitter options. Regenerating from that commit produces no
changes to the generated code.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Broad regeneration and packaging changes warrant human review, particularly without combined live validation and with an unresolved diagnostic failure path.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Retain job diagnostics when document listing fails
If get_job succeeds but document listing fails, including on a later page, the outer catch discards the fetched job and any collected documents. The assertion then omits the job's status, error, and summary—the diagnostics this change is intended to expose. In both describe_job and describe_job_async, catch document enumeration errors separately and retain the available diagnostics alongside the listing error.
Azure/azure-rest-api-specs#47496 merged as 6de3840. Regenerating from it produces no changes to the generated code.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
describe_job(_async) now fetches the job and lists documents in separate try blocks, so a listing failure (even on a later page) no longer discards the job status/error or documents already collected; the listing error is appended instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
The generated _RestField.__get__ replaces the backing list/dict/set in _data
when a mutable field is first read as a property, detaching any reference
previously returned by model["field"]. Wrap __get__ in patch_sdk() to copy
the deserialized contents into the original container instead, until the
emitter template is fixed. Adds unit tests for all dict/property access
patterns plus a guard that fails once the generated code no longer needs
the shim.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
The shim's TODO quoted the emitter template's repository path, which
contains "pygen" and fails the cspell check. Describe the template by
name instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef2497f1-4ec3-4283-9448-f9679c8b7849
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR combines three
azure-health-deidentificationfixes, plus a temporary shim for a regenerated-runtime regression. It supersedes #49625 and #49626.1. Fix CI: stale generated code and regeneration
Problem
The
python - healthdataaiservices - cipipeline has failed on every run since July:reportArgumentType:issubclassreceivestype | Nonein_serialization.py:793and_utils/serialization.py:793.unidiomatic-typecheckin_model_base.pyand_utils/model_base.py, at lines 821, 824 and 830._serialization.py,_model_base.py,_vendor.pyandaio/_vendor.pyare leftovers from an older emitter. They only reference each other. The_utils/copies were also out of date.Fix
@azure-tools/typespec-python0.63.10. The current emitter output passes pyright and pylint without any suppressions.Regeneration side effects (please review)
tsp-location.yaml: the spec moved tospecification/healthdataaiservices/data-plane/HealthDataAIServices. It now points at6de384008f55f42ea97ee0654f237c74fb05ee89, the merge commit of [HealthDataAIServices] Disable Python TypedDict generation for DeID data-plane SDK azure-rest-api-specs#47496 (see the TypedDict item below). Within this spec directory, the only change since the previously pinnedc7d9e3379dd76fe55ce476f7aa312a4fb938615cis thegenerate-typeddictoption intspconfig.yaml. The old pinned commit no longer compiles with the currenttypespec-azure-core(@useDependency(Azure.Core.Versions.v1_0_Preview_2)). I diffed the TypeSpec, and the API surface is unchanged. The only differences are lint#suppresslines, removal of the Azure.Core.Versions dependency,main.tspimportingclient.tsp, and tspconfig output paths.setup.py→pyproject.toml: the emitter now generates PEP 621 packaging.azure-coreis now 1.37.0 (was 1.30.0). README is updated.generate-typeddict: false, the emitter option added in [python] Add DPG TypedDict generation opt-out microsoft/typespec#11372). The emitter's request-body TypedDicts marked read-only fields as required (see [python]: typeddict generating non-input types microsoft/typespec#11562). With the opt-out, notypes.pyis generated and theJSONoverloads match 1.1.0b1. The option is set in the spec'stspconfig.yamlby [HealthDataAIServices] Disable Python TypedDict generation for DeID data-plane SDK azure-rest-api-specs#47496, which is merged.tsp-location.yamlpoints at its merge commit, and regenerating from it produces no changes to the generated code.Accept: application/json:delete_jobreturns 204 with no body.tests/conftest.pyaddsset_custom_default_matcher(excluded_headers="Accept")so the existing recordings, which includeAccepton DELETE, still match. The alternative is re-recording.polling=Falsein the synctest_create_cancel_delete: onceAcceptis excluded, the background LRO poller raced the test's ownget_jobcalls for recorded GET responses. It failed about 3 of 5 runs. The test already polls the job status itself.1.1.0b2 (Unreleased)entry under Other Changes, and_version.pyis bumped to1.1.0b2.api_versiondocstring ("Known values are 2025-07-15-preview and None") is emitter output. It is inaccurate, but runtime behavior is unchanged from 1.1.0b1. It needs an emitter fix (related: [http-client-python] Refineapi_versionargument doc microsoft/typespec#10347) and is not hand-edited here.2. Surface job errors in live test failures; random live job names (from #49626)
Problem
Live runs of
test_create_wait_finish,test_create_wait_finish_async,test_list_job_docs_paginationanddeidentify_documents_async.pykept failing withHttpResponseError: Operation returned an invalid status 'OK'. That message is how azure-core LRO polling reports a job that reachedFailed. It hid the actual job and document errors, so the underlying service issue went undiagnosed for weeks.AZURE_SKIP_LIVE_RECORDING=True, so every live run reused the checked-inuniquifier.confvalue. Any live runs that share resources would then try to create jobs with the same names.Runningafterresult(timeout=120):LROPoller.result(timeout)returns the current resource once the timeout expires. This is not an SDK bug. The tests just never checked whether the poller had finished.Fix
tests/deid_base_test_case.pyaddswait_for_job/wait_for_job_async.job.error, summary counts, and up to 20 non-succeeded documents with their errors.tests/conftest.py: every live run gets a random uniquifier;AZURE_TEST_KEEP_UNIQUIFIERopts out. It is written touniquifier.confonly when recording, so playback stays deterministic.deidentify_documentsandlist_job_documents, sync and async, printJob <name> ended with status <status>: <error>and re-raise on failure. The async samples also close their credential. The README snippet is regenerated fromsamples/deidentify_documents.pywithazpysdk update_snippet.3. Readiness probe in
test-resources-post.ps1(from #49625)Problem
The post-deployment script opens the storage network ACL, then waits a fixed 30 seconds before uploading the test data. The role assignments and the ACL change can take longer to take effect, so live deployments sometimes fail with 403
AuthorizationFailure. The storage cmdlets also didn't stop the script on error, so a failed upload could be missed until tests ran.Fix
Invoke-WithRetryhelper with exponential backoff: it starts at 5s, doubles, caps at 60s, and gives up after about 5 minutes. When it gives up, it throws an error that names the operation and the last error.Get-AzStorageContainer(used as a readiness check) and eachSet-AzStorageBlobContentupload run inside the helper with-ErrorAction Stop.Set-AzStorageAccountalso uses-ErrorAction Stop.Get-ChildItem -File, so only files are uploaded, never directories.4. Temporary shim: keep model list fields bound to one object
Problem
The regenerated
_utils/model_base.pycaches mutable fields when they are read as properties:_RestField.__get__stores a newly deserialized list/dict/set back into the model. Any list previously returned bymodel["field"]is then detached. For example,filters = source["extensions"]; source.extensions; filters.append(".csv")dropped.csvon serialization, which worked in 1.1.0b1. The same emitter change fixed property-first mutations (source.extensions.append(...)), which were lost in 1.1.0b1. No released emitter fixes the dict-then-property case; the template on microsoft/typespecmainis unchanged.Fix
patch_sdk()inazure/health/deidentification/_patch.pywraps the generated_RestField.__get__. If the generated getter replaced the container, the shim copies the new contents into the original object and keeps that one in the model. The cache flag and setter behavior are unchanged, and the wrap is applied once (theaiopackage shares_utilsand imports the root package first).tests/test_model_container_identity.py(unit tests, no recordings) covers all five dict/property access orders forSourceStorageLocation.extensions(list of str) andTaggedPhiEntities.entities(list of models, built from kwargs and from a raw mapping), and checks the serialized output.test_generated_get_still_needs_shimruns the unwrapped generated getter and fails once a regenerated emitter preserves the container itself, which signals that the shim should be deleted.Validation
On the combined branch, run locally with the repo tooling (
azpysdk, pyright 1.1.407, the same as CI):azpysdk black: cleanazpysdk pyright: passazpysdk pylint: 10.00/10azpysdk mypy: passazpysdk update_snippet: README is in syncpytest testsin playback: 32/32 passed in each of 3 runs (13 recorded tests plus 19 new model unit tests). The new tests were confirmed to fail with the shim disabled.test-resources-post.ps1: no PowerShell parse errorsEarlier validation, done on the separate branches before they were combined:
Start-Sleepmocked. This covered success after retries, the time-budget error, the 60s cap, and the final minimum wait.New-TestResources.ps1, using the user's own (non-CI) credentials, torn down withRemove-TestResources.ps1afterwards.Limitations
result(timeout=60/120), matching the README.substdrive, because Windows MAX_PATH breaks the emitter's internal black step.AZURE_TEST_USE_CLI_AUTH=true.Created by Copilot on behalf of Josiah Vinson (Josiah Vinson (@jovinson-ms))