Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions sdk/ai/azure-ai-projects/PostEmitter.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#
# To emit from TypeSpec, run this in the current folder:
#
# tsp-client update --debug ==> to use the commit mentioned in the local tsp-location.yaml to generate
# tsp-client update --debug --save-inputs" ==> To save the local folder TempTypeSpecFiles
# tsp-client update --debug --local-spec-repo <path>" ==> to use your local TypeSpec folder. Path is like:
# D:\src\azure-rest-api-specs\specification\ai-foundry\data-plane\Foundry\src\sdk-python-js-azure-ai-projects
#
# Then run this script to "fix" the emitted code:
# powershell -ExecutionPolicy Bypass -File PostEmitter.ps1
#

# Revert emitted pyprojects.toml, since it overrides the following changes:
# - We added "Programming Language :: Python :: 3.14". The emitter removes it.
# - 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".
# - 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
git restore pyproject.toml


# Edit both _operations.py files to fix missing Foundry-Features HTTP request header in continued list paging calls. Add:
# headers=_headers
# to the end of each of these lines in the BetaXxxOperations classes (do not do this in GA operations classes!)
# "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params
# In emitted code, these first 7 of those lines are associated with GA operations, so start the replacement
# from the 8th occurrence onward.
$gaCount = 7
$old = [char]34 + 'GET' + [char]34 + ', urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params'
$new = $old + ', headers=_headers'
foreach ($f in 'azure\ai\projects\aio\operations\_operations.py', 'azure\ai\projects\operations\_operations.py') {
$c = Get-Content $f -Raw
$parts = $c -split [regex]::Escape($old)
$r = $parts[0]
for ($i = 1; $i -lt $parts.Length; $i++) {
if ($i -le $gaCount) {
$r += $old + $parts[$i]
} else {
$r += $new + $parts[$i]
}
}
Set-Content $f $r -NoNewline
}

# Force streaming in get_session_log_stream for both sync and async operations.
$files = 'azure\ai\projects\operations\_operations.py', 'azure\ai\projects\aio\operations\_operations.py'
foreach ($f in $files) {
$lines = Get-Content $f
$inFunc = $false
for ($i = 0; $i -lt $lines.Length; $i++) {
if ($lines[$i] -match '^\s*(async\s+)?def\s+get_session_log_stream\(') {
$inFunc = $true
continue
}
if ($inFunc -and $lines[$i] -match '^\s*(async\s+)?def\s+\w+\(') {
$inFunc = $false
}
if ($inFunc -and $lines[$i] -match 'kwargs\.pop\(.+stream.+False\)') {
$indent = ([regex]::Match($lines[$i], '^\s*')).Value
$lines[$i] = $indent + '_stream = True'
}
}
Set-Content $f $lines
}

# Fix Sphinx issue in class ToolChoiceAllowed, in "tools" property doc string. The "Required" cannot come at the end of the code-block.
# move it to the end of the text before the code block, and make sure there are no periods after "]".
# .. code-block:: json
#
# [
# { "type": "function", "name": "get_weather" },
# { "type": "mcp", "server_label": "deepwiki" },
# { "type": "image_generation" }
# ]. Required.
(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
(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
(Get-Content azure\ai\projects\models\_models.py) -replace ' \]\. Required\.', ' ]' | Set-Content azure\ai\projects\models\_models.py

# Fix Sphinx docutils warnings in class SessionLogEvent: the generated docstring wraps two long
# ``data:`` JSON lines mid-string inside a ``.. code-block::`` section. The wrapped continuation
# lines have wrong indentation (4 spaces instead of 7), causing "unexpected unindent" warnings.
# Join each broken pair back into one line.
$f = 'azure\ai\projects\models\_models.py'
$c = Get-Content $f -Raw
$c = $c -replace '(Starting server)\r?\n[ \t]+(on port 18080)', '$1 $2'
$c = $c -replace '(Successfully)\r?\n[ \t]+(connected to container\"})\.?', '$1 $2'
Set-Content $f $c -NoNewline
$lines = Get-Content $f
$out = @()
foreach ($line in $lines) {
if ($line -match '^\s*on port 18080' -and $line -notmatch 'data:') { continue }
if ($line -match '^\s*connected to container' -and $line -notmatch 'data:') { continue }
if ($line -match '^\s*data: .*2026-03-10T09:33:17.121Z') {
$out += (' ' + $line.TrimStart())
continue
}
if ($line -match '^\s*data: .*2026-03-10T09:34:52.714Z') {
$out += (' ' + $line.TrimStart())
continue
}
$out += $line
}
Set-Content $f $out

# Fix Sphinx docutils warnings in get_session_log_stream docstrings (sync + async).
# The emitter wraps bullet/code-block lines with insufficient indentation.
$files = 'azure\ai\projects\operations\_operations.py', 'azure\ai\projects\aio\operations\_operations.py'
foreach ($f in $files) {
$c = Get-Content $f -Raw
$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)'
$c = $c -replace '(message\":\"Starting)\r?\n\s+(FoundryCBAgent server on port 8088\"})', '$1 $2'
$c = $c -replace '(message\":\"INFO: Application)\r?\n\s+(startup complete\.\"})', '$1 $2'
$c = $c -replace '(message\":\"Successfully)\r?\n\s+(connected to container\"})', '$1 $2'
$c = $c -replace '(message\":\"No logs since)\r?\n\s+(last 60 seconds\"})', '$1 $2'
Set-Content $f $c -NoNewline
}

# A block of code in the implementation of "list_memories", in both sync
# and async _operations.py files, needs to be moved up. It's emitted in the wrong place,
# in the inline function named "prepare_request". Instead it should be moved up into the
# main body of the "list_memories" method, right after the line `error_map.update(kwargs.pop("error_map", {}) or {})`.
# If you don't do this, the PR pipeline will show failures in Pyright (`error: "body" is unbound (reportUnboundVariable)`)
# and some tests will fail. This is the block of code that needs to move up:
# if body is _Unset:
# if scope is _Unset:
# raise TypeError("missing required argument: scope")
# body = {"scope": scope}
# body = {k: v for k, v in body.items() if v is not None}
# The block inside prepare_request has 12-space indentation; after moving to the main function body it needs 8-space indentation.
# Strategy: Find the last list_memories method, then do a targeted string replacement that moves the block right after error_map.update.
$oldPattern = @"
error_map.update(kwargs.pop("error_map", {}) or {})
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

def prepare_request(_continuation_token=None):
if body is _Unset:
if scope is _Unset:
raise TypeError("missing required argument: scope")
body = {"scope": scope}
body = {k: v for k, v in body.items() if v is not None}

_request = build_beta_memory_stores_list_memories_request(
"@
$newPattern = @"
error_map.update(kwargs.pop("error_map", {}) or {})
if body is _Unset:
if scope is _Unset:
raise TypeError("missing required argument: scope")
body = {"scope": scope}
body = {k: v for k, v in body.items() if v is not 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

def prepare_request(_continuation_token=None):
_request = build_beta_memory_stores_list_memories_request(
"@
$files = 'azure\ai\projects\operations\_operations.py', 'azure\ai\projects\aio\operations\_operations.py'
foreach ($f in $files) {
$c = Get-Content $f -Raw
# Find all occurrences of "def list_memories(" and get the index of the last one
$methodMatches = [regex]::Matches($c, 'def list_memories\(')
if ($methodMatches.Count -eq 0) { continue }
$lastMethodStart = $methodMatches[$methodMatches.Count - 1].Index

# Find the pattern to replace - first occurrence after the last list_memories method
$patternEscaped = [regex]::Escape($oldPattern)
$patternMatches = [regex]::Matches($c, $patternEscaped)
$matchToReplace = $null
foreach ($m in $patternMatches) {
if ($m.Index -gt $lastMethodStart) {
$matchToReplace = $m
break
}
}
if ($matchToReplace -eq $null) { continue }

# Replace only that specific occurrence
$c = $c.Substring(0, $matchToReplace.Index) + $newPattern + $c.Substring($matchToReplace.Index + $matchToReplace.Length)

Set-Content $f $c -NoNewline
}


# Finishing by running 'black' tool to format code.
pip install black
black --config ../../../eng/black-pyproject.toml .
4 changes: 2 additions & 2 deletions sdk/ai/azure-ai-projects/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6954,14 +6954,14 @@ namespace azure.ai.projects.models


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

@overload
def __init__(
self,
*,
items_property: list[OptimizationDatasetItem]
dataset_items: list[OptimizationDatasetItem]
) -> None: ...

@overload
Expand Down
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-projects/api.metadata.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
apiMdSha256: 21533487ff2217a30e5e9e61194466340ea4be034aef29cc66784996eaa298d0
apiMdSha256: f4c05c4124d4d6f1d59ec55aefc17f8184427317294f3a526d8d98d6133c35d8
parserVersion: 0.3.28
pythonVersion: 3.14.3
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-projects/apiview-properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -481,5 +481,5 @@
"azure.ai.projects.operations.IndexesOperations.create_or_update": "Azure.AI.Projects.Indexes.createOrUpdateVersion",
"azure.ai.projects.aio.operations.IndexesOperations.create_or_update": "Azure.AI.Projects.Indexes.createOrUpdateVersion"
},
"CrossLanguageVersion": "0ee459332041"
"CrossLanguageVersion": "54953c829a31"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9013,14 +9013,11 @@ def list_memories(
304: ResourceNotModifiedError,
}
error_map.update(kwargs.pop("error_map", {}) or {})

# BUG? These lines were inside the prepare_request() method. Moved here instead.
if body is _Unset:
if scope is _Unset:
raise TypeError("missing required argument: scope")
body = {"scope": scope}
body = {k: v for k, v in body.items() if v is not None}

content_type = content_type or "application/json"
_content = None
if isinstance(body, (IOBase, bytes)):
Expand All @@ -9029,12 +9026,6 @@ def list_memories(
_content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore

def prepare_request(_continuation_token=None):
# if body is _Unset:
# if scope is _Unset:
# raise TypeError("missing required argument: scope")
# body = {"scope": scope}
# body = {k: v for k, v in body.items() if v is not None}

_request = build_beta_memory_stores_list_memories_request(
name=name,
kind=kind,
Expand Down
10 changes: 5 additions & 5 deletions sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10824,23 +10824,23 @@ class OptimizationInlineDatasetInput(OptimizationDatasetInput, discriminator="in
:ivar type: Dataset input type discriminator. Required. Inline dataset — items are provided
directly in the request body.
:vartype type: str or ~azure.ai.projects.models.INLINE
:ivar items_property: Dataset items. Required.
:vartype items_property: list[~azure.ai.projects.models.OptimizationDatasetItem]
:ivar dataset_items: Dataset items. Required.
:vartype dataset_items: list[~azure.ai.projects.models.OptimizationDatasetItem]
"""

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

@overload
def __init__(
self,
*,
items_property: list["_models.OptimizationDatasetItem"],
dataset_items: list["_models.OptimizationDatasetItem"],
) -> None: ...

@overload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12436,14 +12436,11 @@ def list_memories(
304: ResourceNotModifiedError,
}
error_map.update(kwargs.pop("error_map", {}) or {})

# BUG? These lines were inside the prepare_request() method. Moved here instead.
if body is _Unset:
if scope is _Unset:
raise TypeError("missing required argument: scope")
body = {"scope": scope}
body = {k: v for k, v in body.items() if v is not None}

content_type = content_type or "application/json"
_content = None
if isinstance(body, (IOBase, bytes)):
Expand All @@ -12452,12 +12449,6 @@ def list_memories(
_content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore

def prepare_request(_continuation_token=None):
# if body is _Unset:
# if scope is _Unset:
# raise TypeError("missing required argument: scope")
# body = {"scope": scope}
# body = {k: v for k, v in body.items() if v is not None}

_request = build_beta_memory_stores_list_memories_request(
name=name,
kind=kind,
Expand Down
Loading
Loading