Skip to content

Commit d20c8c4

Browse files
authored
Update post emitter script (#47525)
1 parent 81ec85b commit d20c8c4

8 files changed

Lines changed: 202 additions & 104 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#
2+
# To emit from TypeSpec, run this in the current folder:
3+
#
4+
# tsp-client update --debug ==> to use the commit mentioned in the local tsp-location.yaml to generate
5+
# tsp-client update --debug --save-inputs" ==> To save the local folder TempTypeSpecFiles
6+
# tsp-client update --debug --local-spec-repo <path>" ==> to use your local TypeSpec folder. Path is like:
7+
# D:\src\azure-rest-api-specs\specification\ai-foundry\data-plane\Foundry\src\sdk-python-js-azure-ai-projects
8+
#
9+
# Then run this script to "fix" the emitted code:
10+
# powershell -ExecutionPolicy Bypass -File PostEmitter.ps1
11+
#
12+
13+
# Revert emitted pyprojects.toml, since it overrides the following changes:
14+
# - We added "Programming Language :: Python :: 3.14". The emitter removes it.
15+
# - The emitter uses lower case "i" in "Ai". I want to keep it upper case in the description field: "Microsoft Corporation Azure AI Projects Client Library for Python".
16+
# - We want a vanity link for the "repository" value, deep linking to the SDK folder (not root of repo): https://aka.ms/azsdk/azure-ai-projects-v2/python/code
17+
git restore pyproject.toml
18+
19+
20+
# Edit both _operations.py files to fix missing Foundry-Features HTTP request header in continued list paging calls. Add:
21+
# headers=_headers
22+
# to the end of each of these lines in the BetaXxxOperations classes (do not do this in GA operations classes!)
23+
# "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params
24+
# In emitted code, these first 7 of those lines are associated with GA operations, so start the replacement
25+
# from the 8th occurrence onward.
26+
$gaCount = 7
27+
$old = [char]34 + 'GET' + [char]34 + ', urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params'
28+
$new = $old + ', headers=_headers'
29+
foreach ($f in 'azure\ai\projects\aio\operations\_operations.py', 'azure\ai\projects\operations\_operations.py') {
30+
$c = Get-Content $f -Raw
31+
$parts = $c -split [regex]::Escape($old)
32+
$r = $parts[0]
33+
for ($i = 1; $i -lt $parts.Length; $i++) {
34+
if ($i -le $gaCount) {
35+
$r += $old + $parts[$i]
36+
} else {
37+
$r += $new + $parts[$i]
38+
}
39+
}
40+
Set-Content $f $r -NoNewline
41+
}
42+
43+
# Force streaming in get_session_log_stream for both sync and async operations.
44+
$files = 'azure\ai\projects\operations\_operations.py', 'azure\ai\projects\aio\operations\_operations.py'
45+
foreach ($f in $files) {
46+
$lines = Get-Content $f
47+
$inFunc = $false
48+
for ($i = 0; $i -lt $lines.Length; $i++) {
49+
if ($lines[$i] -match '^\s*(async\s+)?def\s+get_session_log_stream\(') {
50+
$inFunc = $true
51+
continue
52+
}
53+
if ($inFunc -and $lines[$i] -match '^\s*(async\s+)?def\s+\w+\(') {
54+
$inFunc = $false
55+
}
56+
if ($inFunc -and $lines[$i] -match 'kwargs\.pop\(.+stream.+False\)') {
57+
$indent = ([regex]::Match($lines[$i], '^\s*')).Value
58+
$lines[$i] = $indent + '_stream = True'
59+
}
60+
}
61+
Set-Content $f $lines
62+
}
63+
64+
# Fix Sphinx issue in class ToolChoiceAllowed, in "tools" property doc string. The "Required" cannot come at the end of the code-block.
65+
# move it to the end of the text before the code block, and make sure there are no periods after "]".
66+
# .. code-block:: json
67+
#
68+
# [
69+
# { "type": "function", "name": "get_weather" },
70+
# { "type": "mcp", "server_label": "deepwiki" },
71+
# { "type": "image_generation" }
72+
# ]. Required.
73+
(Get-Content azure\ai\projects\models\_models.py) -replace 'Responses API, the list of tool definitions might look like:', 'Responses API, the list of tool definitions might look like the following. Required.' | Set-Content azure\ai\projects\models\_models.py
74+
(Get-Content azure\ai\projects\models\_models.py) -replace 'list of tool definitions might look like:', 'list of tool definitions might look like the following. Required.' | Set-Content azure\ai\projects\models\_models.py
75+
(Get-Content azure\ai\projects\models\_models.py) -replace ' \]\. Required\.', ' ]' | Set-Content azure\ai\projects\models\_models.py
76+
77+
# Fix Sphinx docutils warnings in class SessionLogEvent: the generated docstring wraps two long
78+
# ``data:`` JSON lines mid-string inside a ``.. code-block::`` section. The wrapped continuation
79+
# lines have wrong indentation (4 spaces instead of 7), causing "unexpected unindent" warnings.
80+
# Join each broken pair back into one line.
81+
$f = 'azure\ai\projects\models\_models.py'
82+
$c = Get-Content $f -Raw
83+
$c = $c -replace '(Starting server)\r?\n[ \t]+(on port 18080)', '$1 $2'
84+
$c = $c -replace '(Successfully)\r?\n[ \t]+(connected to container\"})\.?', '$1 $2'
85+
Set-Content $f $c -NoNewline
86+
$lines = Get-Content $f
87+
$out = @()
88+
foreach ($line in $lines) {
89+
if ($line -match '^\s*on port 18080' -and $line -notmatch 'data:') { continue }
90+
if ($line -match '^\s*connected to container' -and $line -notmatch 'data:') { continue }
91+
if ($line -match '^\s*data: .*2026-03-10T09:33:17.121Z') {
92+
$out += (' ' + $line.TrimStart())
93+
continue
94+
}
95+
if ($line -match '^\s*data: .*2026-03-10T09:34:52.714Z') {
96+
$out += (' ' + $line.TrimStart())
97+
continue
98+
}
99+
$out += $line
100+
}
101+
Set-Content $f $out
102+
103+
# Fix Sphinx docutils warnings in get_session_log_stream docstrings (sync + async).
104+
# The emitter wraps bullet/code-block lines with insufficient indentation.
105+
$files = 'azure\ai\projects\operations\_operations.py', 'azure\ai\projects\aio\operations\_operations.py'
106+
foreach ($f in $files) {
107+
$c = Get-Content $f -Raw
108+
$c = $c -replace 'schema\r?\n\s+is not contractual and may include additional keys or change format\r?\n\s+over time [^\r\n]*clients should treat it as an opaque string\)', 'schema is not contractual and may include additional keys or change format over time; clients should treat it as an opaque string)'
109+
$c = $c -replace '(message\":\"Starting)\r?\n\s+(FoundryCBAgent server on port 8088\"})', '$1 $2'
110+
$c = $c -replace '(message\":\"INFO: Application)\r?\n\s+(startup complete\.\"})', '$1 $2'
111+
$c = $c -replace '(message\":\"Successfully)\r?\n\s+(connected to container\"})', '$1 $2'
112+
$c = $c -replace '(message\":\"No logs since)\r?\n\s+(last 60 seconds\"})', '$1 $2'
113+
Set-Content $f $c -NoNewline
114+
}
115+
116+
# A block of code in the implementation of "list_memories", in both sync
117+
# and async _operations.py files, needs to be moved up. It's emitted in the wrong place,
118+
# in the inline function named "prepare_request". Instead it should be moved up into the
119+
# main body of the "list_memories" method, right after the line `error_map.update(kwargs.pop("error_map", {}) or {})`.
120+
# If you don't do this, the PR pipeline will show failures in Pyright (`error: "body" is unbound (reportUnboundVariable)`)
121+
# and some tests will fail. This is the block of code that needs to move up:
122+
# if body is _Unset:
123+
# if scope is _Unset:
124+
# raise TypeError("missing required argument: scope")
125+
# body = {"scope": scope}
126+
# body = {k: v for k, v in body.items() if v is not None}
127+
# The block inside prepare_request has 12-space indentation; after moving to the main function body it needs 8-space indentation.
128+
# Strategy: Find the last list_memories method, then do a targeted string replacement that moves the block right after error_map.update.
129+
$oldPattern = @"
130+
error_map.update(kwargs.pop("error_map", {}) or {})
131+
content_type = content_type or "application/json"
132+
_content = None
133+
if isinstance(body, (IOBase, bytes)):
134+
_content = body
135+
else:
136+
_content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore
137+
138+
def prepare_request(_continuation_token=None):
139+
if body is _Unset:
140+
if scope is _Unset:
141+
raise TypeError("missing required argument: scope")
142+
body = {"scope": scope}
143+
body = {k: v for k, v in body.items() if v is not None}
144+
145+
_request = build_beta_memory_stores_list_memories_request(
146+
"@
147+
$newPattern = @"
148+
error_map.update(kwargs.pop("error_map", {}) or {})
149+
if body is _Unset:
150+
if scope is _Unset:
151+
raise TypeError("missing required argument: scope")
152+
body = {"scope": scope}
153+
body = {k: v for k, v in body.items() if v is not None}
154+
content_type = content_type or "application/json"
155+
_content = None
156+
if isinstance(body, (IOBase, bytes)):
157+
_content = body
158+
else:
159+
_content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore
160+
161+
def prepare_request(_continuation_token=None):
162+
_request = build_beta_memory_stores_list_memories_request(
163+
"@
164+
$files = 'azure\ai\projects\operations\_operations.py', 'azure\ai\projects\aio\operations\_operations.py'
165+
foreach ($f in $files) {
166+
$c = Get-Content $f -Raw
167+
# Find all occurrences of "def list_memories(" and get the index of the last one
168+
$methodMatches = [regex]::Matches($c, 'def list_memories\(')
169+
if ($methodMatches.Count -eq 0) { continue }
170+
$lastMethodStart = $methodMatches[$methodMatches.Count - 1].Index
171+
172+
# Find the pattern to replace - first occurrence after the last list_memories method
173+
$patternEscaped = [regex]::Escape($oldPattern)
174+
$patternMatches = [regex]::Matches($c, $patternEscaped)
175+
$matchToReplace = $null
176+
foreach ($m in $patternMatches) {
177+
if ($m.Index -gt $lastMethodStart) {
178+
$matchToReplace = $m
179+
break
180+
}
181+
}
182+
if ($matchToReplace -eq $null) { continue }
183+
184+
# Replace only that specific occurrence
185+
$c = $c.Substring(0, $matchToReplace.Index) + $newPattern + $c.Substring($matchToReplace.Index + $matchToReplace.Length)
186+
187+
Set-Content $f $c -NoNewline
188+
}
189+
190+
191+
# Finishing by running 'black' tool to format code.
192+
pip install black
193+
black --config ../../../eng/black-pyproject.toml .

sdk/ai/azure-ai-projects/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6954,14 +6954,14 @@ namespace azure.ai.projects.models
69546954

69556955

69566956
class azure.ai.projects.models.OptimizationInlineDatasetInput(OptimizationDatasetInput, discriminator='inline'):
6957-
items_property: list[OptimizationDatasetItem]
6957+
dataset_items: list[OptimizationDatasetItem]
69586958
type: Literal[OptimizationDatasetInputType.INLINE]
69596959

69606960
@overload
69616961
def __init__(
69626962
self,
69636963
*,
6964-
items_property: list[OptimizationDatasetItem]
6964+
dataset_items: list[OptimizationDatasetItem]
69656965
) -> None: ...
69666966

69676967
@overload
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
apiMdSha256: 21533487ff2217a30e5e9e61194466340ea4be034aef29cc66784996eaa298d0
1+
apiMdSha256: f4c05c4124d4d6f1d59ec55aefc17f8184427317294f3a526d8d98d6133c35d8
22
parserVersion: 0.3.28
33
pythonVersion: 3.14.3

sdk/ai/azure-ai-projects/apiview-properties.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,5 +481,5 @@
481481
"azure.ai.projects.operations.IndexesOperations.create_or_update": "Azure.AI.Projects.Indexes.createOrUpdateVersion",
482482
"azure.ai.projects.aio.operations.IndexesOperations.create_or_update": "Azure.AI.Projects.Indexes.createOrUpdateVersion"
483483
},
484-
"CrossLanguageVersion": "0ee459332041"
484+
"CrossLanguageVersion": "54953c829a31"
485485
}

sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9013,14 +9013,11 @@ def list_memories(
90139013
304: ResourceNotModifiedError,
90149014
}
90159015
error_map.update(kwargs.pop("error_map", {}) or {})
9016-
9017-
# BUG? These lines were inside the prepare_request() method. Moved here instead.
90189016
if body is _Unset:
90199017
if scope is _Unset:
90209018
raise TypeError("missing required argument: scope")
90219019
body = {"scope": scope}
90229020
body = {k: v for k, v in body.items() if v is not None}
9023-
90249021
content_type = content_type or "application/json"
90259022
_content = None
90269023
if isinstance(body, (IOBase, bytes)):
@@ -9029,12 +9026,6 @@ def list_memories(
90299026
_content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore
90309027

90319028
def prepare_request(_continuation_token=None):
9032-
# if body is _Unset:
9033-
# if scope is _Unset:
9034-
# raise TypeError("missing required argument: scope")
9035-
# body = {"scope": scope}
9036-
# body = {k: v for k, v in body.items() if v is not None}
9037-
90389029
_request = build_beta_memory_stores_list_memories_request(
90399030
name=name,
90409031
kind=kind,

sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10824,23 +10824,23 @@ class OptimizationInlineDatasetInput(OptimizationDatasetInput, discriminator="in
1082410824
:ivar type: Dataset input type discriminator. Required. Inline dataset — items are provided
1082510825
directly in the request body.
1082610826
:vartype type: str or ~azure.ai.projects.models.INLINE
10827-
:ivar items_property: Dataset items. Required.
10828-
:vartype items_property: list[~azure.ai.projects.models.OptimizationDatasetItem]
10827+
:ivar dataset_items: Dataset items. Required.
10828+
:vartype dataset_items: list[~azure.ai.projects.models.OptimizationDatasetItem]
1082910829
"""
1083010830

1083110831
type: Literal[OptimizationDatasetInputType.INLINE] = rest_discriminator(name="type", visibility=["read", "create", "update", "delete", "query"]) # type: ignore
1083210832
"""Dataset input type discriminator. Required. Inline dataset — items are provided directly in the
1083310833
request body."""
10834-
items_property: list["_models.OptimizationDatasetItem"] = rest_field(
10835-
name="items", visibility=["read", "create", "update", "delete", "query"], original_tsp_name="items"
10834+
dataset_items: list["_models.OptimizationDatasetItem"] = rest_field(
10835+
name="items", visibility=["read", "create", "update", "delete", "query"]
1083610836
)
1083710837
"""Dataset items. Required."""
1083810838

1083910839
@overload
1084010840
def __init__(
1084110841
self,
1084210842
*,
10843-
items_property: list["_models.OptimizationDatasetItem"],
10843+
dataset_items: list["_models.OptimizationDatasetItem"],
1084410844
) -> None: ...
1084510845

1084610846
@overload

sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12436,14 +12436,11 @@ def list_memories(
1243612436
304: ResourceNotModifiedError,
1243712437
}
1243812438
error_map.update(kwargs.pop("error_map", {}) or {})
12439-
12440-
# BUG? These lines were inside the prepare_request() method. Moved here instead.
1244112439
if body is _Unset:
1244212440
if scope is _Unset:
1244312441
raise TypeError("missing required argument: scope")
1244412442
body = {"scope": scope}
1244512443
body = {k: v for k, v in body.items() if v is not None}
12446-
1244712444
content_type = content_type or "application/json"
1244812445
_content = None
1244912446
if isinstance(body, (IOBase, bytes)):
@@ -12452,12 +12449,6 @@ def list_memories(
1245212449
_content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore
1245312450

1245412451
def prepare_request(_continuation_token=None):
12455-
# if body is _Unset:
12456-
# if scope is _Unset:
12457-
# raise TypeError("missing required argument: scope")
12458-
# body = {"scope": scope}
12459-
# body = {k: v for k, v in body.items() if v is not None}
12460-
1246112452
_request = build_beta_memory_stores_list_memories_request(
1246212453
name=name,
1246312454
kind=kind,

0 commit comments

Comments
 (0)