@typespec/http-client-python 0.30.0-alpha.20260529.2 → 0.30.0-alpha.20260601.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -242,6 +242,7 @@ class GeneralSerializer(BaseSerializer):
242
242
  ImportType.LOCAL,
243
243
  )
244
244
  file_import.add_import("json", ImportType.STDLIB)
245
+ file_import.add_import("os", ImportType.STDLIB)
245
246
 
246
247
  return template.render(
247
248
  code_model=self.code_model,
@@ -76,23 +76,60 @@ def serialize_multipart_data_entry(data_entry: Any) -> Any:
76
76
  return json.dumps(data_entry, cls=SdkJSONEncoder, exclude_readonly=True)
77
77
  return data_entry
78
78
 
79
+ def _normalize_multipart_file_entry(field_name: str, entry: Any, index: int) -> Any:
80
+ """Ensure a multipart file entry carries a filename for Content-Disposition.
81
+
82
+ Servers distinguish file parts from plain form fields by the presence of
83
+ ``filename=`` in the ``Content-Disposition`` header. When callers pass
84
+ bare bytes/str/IO the HTTP client omits the filename and the server may
85
+ reject the upload. This helper wraps bare values into a (filename, content)
86
+ tuple, deriving the name from IO.name when available.
87
+
88
+ :param str field_name: The multipart field name used as a filename fallback.
89
+ :param entry: The user-provided file entry (tuple, bytes, str, or IO).
90
+ :type entry: any
91
+ :param int index: The positional index of the entry within the field, used
92
+ to disambiguate fallback filenames when multiple entries are provided.
93
+ :return: Either the original tuple entry, or a ``(filename, content)`` tuple
94
+ wrapping the bare value.
95
+ :rtype: any
96
+ """
97
+ if isinstance(entry, tuple):
98
+ return entry
99
+ filename: Optional[str] = None
100
+ name_attr = getattr(entry, "name", None)
101
+ if isinstance(name_attr, str) and name_attr:
102
+ filename = os.path.basename(name_attr)
103
+ if not filename:
104
+ filename = f"{field_name}_{index}" if index else field_name
105
+
106
+ # Return a 3-tuple with an explicit "application/octet-stream" content type.
107
+ # A 2-tuple (filename, content) would leave the part's Content-Type unset, and
108
+ # the sdk core library only defaults to "application/octet-stream" for bare
109
+ # (non-tuple) values - a tuple bypasses that default and falls back to the
110
+ # HTTP "text/plain" default instead. Setting it explicitly preserves the
111
+ # pre-existing behavior for bare bytes/IO across all transports.
112
+ return (filename, entry, "application/octet-stream")
113
+
79
114
  def prepare_multipart_form_data(
80
115
  body: Mapping[str, Any], multipart_fields: list[str], data_fields: list[str]
81
116
  ) -> list[FileType]:
82
117
  files: list[FileType] = []
83
- for multipart_field in multipart_fields:
84
- multipart_entry = body.get(multipart_field)
85
- if isinstance(multipart_entry, list):
86
- files.extend([(multipart_field, e) for e in multipart_entry ])
87
- elif multipart_entry:
88
- files.append((multipart_field, multipart_entry))
89
118
 
90
- # if files is empty, sdk core library can't handle multipart/form-data correctly, so
91
- # we put data fields into files with filename as None to avoid that scenario.
119
+ # Data fields first so streaming server-side parsers see metadata before
120
+ # binary file parts.
92
121
  for data_field in data_fields:
93
122
  data_entry = body.get(data_field)
94
123
  if data_entry:
95
124
  files.append((data_field, str(serialize_multipart_data_entry(data_entry))))
96
125
 
126
+ for multipart_field in multipart_fields:
127
+ multipart_entry = body.get(multipart_field)
128
+ if isinstance(multipart_entry, list):
129
+ for idx, e in enumerate(multipart_entry):
130
+ files.append((multipart_field, _normalize_multipart_file_entry(multipart_field, e, idx)))
131
+ elif multipart_entry is not None:
132
+ files.append((multipart_field, _normalize_multipart_file_entry(multipart_field, multipart_entry, 0)))
133
+
97
134
  return files
98
135
  {% endif %}
@@ -158,6 +158,8 @@ CLOUD_SETTING = {
158
158
  "isTypingOnly": True,
159
159
  },
160
160
  }
161
+ STANDARD_IF_MATCH_WIRE_NAME = "if-match"
162
+ STANDARD_IF_NONE_MATCH_WIRE_NAME = "if-none-match"
161
163
 
162
164
 
163
165
  def get_wire_name_lower(parameter: dict[str, Any]) -> str:
@@ -169,6 +171,106 @@ def _get_etag_role(parameter: dict[str, Any]) -> Optional[str]:
169
171
  return parameter.get("etagRole")
170
172
 
171
173
 
174
+ def _pick_etag_slot(
175
+ candidates: list[dict[str, Any]], standard_wire_name: str
176
+ ) -> Optional[dict[str, Any]]:
177
+ """Choose which etag-typed header should be promoted to the etag/match_condition slot.
178
+
179
+ When more than one etag-typed header is present in an operation, prefer the
180
+ standard If-Match/If-None-Match header (matched on wireName). Otherwise
181
+ fall back to the first candidate. Returns None if there are no candidates.
182
+ """
183
+ if not candidates:
184
+ return None
185
+ for c in candidates:
186
+ if get_wire_name_lower(c) == standard_wire_name:
187
+ return c
188
+ return candidates[0]
189
+
190
+
191
+ def _resolve_etag_pair(
192
+ if_match_candidates: list[dict[str, Any]],
193
+ if_none_match_candidates: list[dict[str, Any]],
194
+ ) -> tuple[Optional[dict[str, Any]], Optional[dict[str, Any]]]:
195
+ """Select and reconcile the etag header pair for an operation.
196
+
197
+ When multiple etag-typed headers are present, prefer the standard
198
+ If-Match / If-None-Match pair. Synthesize a missing partner when only
199
+ one side is present, and strip etagRole from non-selected candidates.
200
+
201
+ Returns (property_if_match, property_if_none_match) — both None when
202
+ there are no etag candidates.
203
+ """
204
+ property_if_match = _pick_etag_slot(if_match_candidates, STANDARD_IF_MATCH_WIRE_NAME)
205
+ property_if_none_match = _pick_etag_slot(if_none_match_candidates, STANDARD_IF_NONE_MATCH_WIRE_NAME)
206
+
207
+ # Ensure the promoted pair come from the same family. When one slot is
208
+ # standard and the other custom (cross-family), replace the custom slot
209
+ # with a synthetic standard partner. Also synthesize the missing partner
210
+ # when only one side is present.
211
+ if property_if_match and property_if_none_match:
212
+ match_is_std = get_wire_name_lower(property_if_match) == STANDARD_IF_MATCH_WIRE_NAME
213
+ none_match_is_std = get_wire_name_lower(property_if_none_match) == STANDARD_IF_NONE_MATCH_WIRE_NAME
214
+ if match_is_std and not none_match_is_std:
215
+ property_if_none_match = property_if_match.copy()
216
+ property_if_none_match["wireName"] = STANDARD_IF_NONE_MATCH_WIRE_NAME
217
+ property_if_none_match["etagRole"] = "ifNoneMatch"
218
+ elif none_match_is_std and not match_is_std:
219
+ property_if_match = property_if_none_match.copy()
220
+ property_if_match["wireName"] = STANDARD_IF_MATCH_WIRE_NAME
221
+ property_if_match["etagRole"] = "ifMatch"
222
+ elif not property_if_match and property_if_none_match:
223
+ property_if_match = property_if_none_match.copy()
224
+ property_if_match["wireName"] = STANDARD_IF_MATCH_WIRE_NAME
225
+ property_if_match["etagRole"] = "ifMatch"
226
+ elif property_if_match and not property_if_none_match:
227
+ property_if_none_match = property_if_match.copy()
228
+ property_if_none_match["wireName"] = STANDARD_IF_NONE_MATCH_WIRE_NAME
229
+ property_if_none_match["etagRole"] = "ifNoneMatch"
230
+
231
+ for c in if_match_candidates:
232
+ if c is not property_if_match:
233
+ c.pop("etagRole", None)
234
+ for c in if_none_match_candidates:
235
+ if c is not property_if_none_match:
236
+ c.pop("etagRole", None)
237
+
238
+ return property_if_match, property_if_none_match
239
+
240
+
241
+ def _process_operation_etag_headers(
242
+ operation: dict[str, Any],
243
+ client: dict[str, Any],
244
+ version_tolerant: bool,
245
+ ) -> None:
246
+ """Collect etag candidates from *operation*, resolve the promoted pair,
247
+ and update *operation* / *client* accordingly."""
248
+ if_match_candidates: list[dict[str, Any]] = []
249
+ if_none_match_candidates: list[dict[str, Any]] = []
250
+ for p in operation["parameters"]:
251
+ wire_name_lower = get_wire_name_lower(p)
252
+ if p["location"] == "header" and wire_name_lower == "client-request-id":
253
+ client["requestIdHeaderName"] = wire_name_lower
254
+ if version_tolerant and p["location"] == "header":
255
+ role = _get_etag_role(p)
256
+ if role == "ifMatch":
257
+ if_match_candidates.append(p)
258
+ elif role == "ifNoneMatch":
259
+ if_none_match_candidates.append(p)
260
+
261
+ property_if_match, property_if_none_match = _resolve_etag_pair(
262
+ if_match_candidates, if_none_match_candidates
263
+ )
264
+
265
+ if property_if_match and property_if_none_match:
266
+ etag_params = {id(property_if_match), id(property_if_none_match)}
267
+ operation["parameters"] = [
268
+ item for item in operation["parameters"] if id(item) not in etag_params
269
+ ] + [property_if_match, property_if_none_match]
270
+ operation["hasEtag"] = True
271
+ client["hasEtag"] = True
272
+
273
+
172
274
  def headers_convert(yaml_data: dict[str, Any], replace_data: Any) -> None:
173
275
  if isinstance(replace_data, dict):
174
276
  for k, v in replace_data.items():
@@ -313,40 +415,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
313
415
  yaml_data["builderPadName"] = to_snake_case(prop_name)
314
416
  for og in yaml_data.get("operationGroups", []):
315
417
  for o in og["operations"]:
316
- property_if_match = None
317
- property_if_none_match = None
318
- for p in o["parameters"]:
319
- wire_name_lower = get_wire_name_lower(p)
320
- if p["location"] == "header" and wire_name_lower == "client-request-id":
321
- yaml_data["requestIdHeaderName"] = wire_name_lower
322
- if self.version_tolerant and p["location"] == "header":
323
- role = _get_etag_role(p)
324
- if role == "ifMatch" and not property_if_match:
325
- property_if_match = p
326
- elif role == "ifNoneMatch" and not property_if_none_match:
327
- property_if_none_match = p
328
- # pylint: disable=line-too-long
329
- # some service(e.g. https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cosmos-db/data-plane/Microsoft.Tables/preview/2019-02-02/table.json)
330
- # only has one, so we need to add "if-none-match" or "if-match" if it's missing
331
- if not property_if_match and property_if_none_match:
332
- property_if_match = property_if_none_match.copy()
333
- property_if_match["wireName"] = "if-match"
334
- property_if_match["etagRole"] = "ifMatch"
335
- if not property_if_none_match and property_if_match:
336
- property_if_none_match = property_if_match.copy()
337
- property_if_none_match["wireName"] = "if-none-match"
338
- property_if_none_match["etagRole"] = "ifNoneMatch"
339
-
340
- if property_if_match and property_if_none_match:
341
- # arrange if-match and if-none-match to the end of parameters
342
- etag_params = {id(property_if_match), id(property_if_none_match)}
343
- o["parameters"] = [item for item in o["parameters"] if id(item) not in etag_params] + [
344
- property_if_match,
345
- property_if_none_match,
346
- ]
347
-
348
- o["hasEtag"] = True
349
- yaml_data["hasEtag"] = True
418
+ _process_operation_etag_headers(o, yaml_data, self.version_tolerant)
350
419
 
351
420
  # add client signature cloud_setting for arm
352
421
  if self.azure_arm and yaml_data["parameters"]:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1",
3
- "pip_version": "26.1.1",
3
+ "pip_version": "26.1.2",
4
4
  "install": [
5
5
  {
6
6
  "download_info": {
@@ -242,6 +242,7 @@ class GeneralSerializer(BaseSerializer):
242
242
  ImportType.LOCAL,
243
243
  )
244
244
  file_import.add_import("json", ImportType.STDLIB)
245
+ file_import.add_import("os", ImportType.STDLIB)
245
246
 
246
247
  return template.render(
247
248
  code_model=self.code_model,
@@ -76,23 +76,60 @@ def serialize_multipart_data_entry(data_entry: Any) -> Any:
76
76
  return json.dumps(data_entry, cls=SdkJSONEncoder, exclude_readonly=True)
77
77
  return data_entry
78
78
 
79
+ def _normalize_multipart_file_entry(field_name: str, entry: Any, index: int) -> Any:
80
+ """Ensure a multipart file entry carries a filename for Content-Disposition.
81
+
82
+ Servers distinguish file parts from plain form fields by the presence of
83
+ ``filename=`` in the ``Content-Disposition`` header. When callers pass
84
+ bare bytes/str/IO the HTTP client omits the filename and the server may
85
+ reject the upload. This helper wraps bare values into a (filename, content)
86
+ tuple, deriving the name from IO.name when available.
87
+
88
+ :param str field_name: The multipart field name used as a filename fallback.
89
+ :param entry: The user-provided file entry (tuple, bytes, str, or IO).
90
+ :type entry: any
91
+ :param int index: The positional index of the entry within the field, used
92
+ to disambiguate fallback filenames when multiple entries are provided.
93
+ :return: Either the original tuple entry, or a ``(filename, content)`` tuple
94
+ wrapping the bare value.
95
+ :rtype: any
96
+ """
97
+ if isinstance(entry, tuple):
98
+ return entry
99
+ filename: Optional[str] = None
100
+ name_attr = getattr(entry, "name", None)
101
+ if isinstance(name_attr, str) and name_attr:
102
+ filename = os.path.basename(name_attr)
103
+ if not filename:
104
+ filename = f"{field_name}_{index}" if index else field_name
105
+
106
+ # Return a 3-tuple with an explicit "application/octet-stream" content type.
107
+ # A 2-tuple (filename, content) would leave the part's Content-Type unset, and
108
+ # the sdk core library only defaults to "application/octet-stream" for bare
109
+ # (non-tuple) values - a tuple bypasses that default and falls back to the
110
+ # HTTP "text/plain" default instead. Setting it explicitly preserves the
111
+ # pre-existing behavior for bare bytes/IO across all transports.
112
+ return (filename, entry, "application/octet-stream")
113
+
79
114
  def prepare_multipart_form_data(
80
115
  body: Mapping[str, Any], multipart_fields: list[str], data_fields: list[str]
81
116
  ) -> list[FileType]:
82
117
  files: list[FileType] = []
83
- for multipart_field in multipart_fields:
84
- multipart_entry = body.get(multipart_field)
85
- if isinstance(multipart_entry, list):
86
- files.extend([(multipart_field, e) for e in multipart_entry ])
87
- elif multipart_entry:
88
- files.append((multipart_field, multipart_entry))
89
118
 
90
- # if files is empty, sdk core library can't handle multipart/form-data correctly, so
91
- # we put data fields into files with filename as None to avoid that scenario.
119
+ # Data fields first so streaming server-side parsers see metadata before
120
+ # binary file parts.
92
121
  for data_field in data_fields:
93
122
  data_entry = body.get(data_field)
94
123
  if data_entry:
95
124
  files.append((data_field, str(serialize_multipart_data_entry(data_entry))))
96
125
 
126
+ for multipart_field in multipart_fields:
127
+ multipart_entry = body.get(multipart_field)
128
+ if isinstance(multipart_entry, list):
129
+ for idx, e in enumerate(multipart_entry):
130
+ files.append((multipart_field, _normalize_multipart_file_entry(multipart_field, e, idx)))
131
+ elif multipart_entry is not None:
132
+ files.append((multipart_field, _normalize_multipart_file_entry(multipart_field, multipart_entry, 0)))
133
+
97
134
  return files
98
135
  {% endif %}
@@ -158,6 +158,8 @@ CLOUD_SETTING = {
158
158
  "isTypingOnly": True,
159
159
  },
160
160
  }
161
+ STANDARD_IF_MATCH_WIRE_NAME = "if-match"
162
+ STANDARD_IF_NONE_MATCH_WIRE_NAME = "if-none-match"
161
163
 
162
164
 
163
165
  def get_wire_name_lower(parameter: dict[str, Any]) -> str:
@@ -169,6 +171,106 @@ def _get_etag_role(parameter: dict[str, Any]) -> Optional[str]:
169
171
  return parameter.get("etagRole")
170
172
 
171
173
 
174
+ def _pick_etag_slot(
175
+ candidates: list[dict[str, Any]], standard_wire_name: str
176
+ ) -> Optional[dict[str, Any]]:
177
+ """Choose which etag-typed header should be promoted to the etag/match_condition slot.
178
+
179
+ When more than one etag-typed header is present in an operation, prefer the
180
+ standard If-Match/If-None-Match header (matched on wireName). Otherwise
181
+ fall back to the first candidate. Returns None if there are no candidates.
182
+ """
183
+ if not candidates:
184
+ return None
185
+ for c in candidates:
186
+ if get_wire_name_lower(c) == standard_wire_name:
187
+ return c
188
+ return candidates[0]
189
+
190
+
191
+ def _resolve_etag_pair(
192
+ if_match_candidates: list[dict[str, Any]],
193
+ if_none_match_candidates: list[dict[str, Any]],
194
+ ) -> tuple[Optional[dict[str, Any]], Optional[dict[str, Any]]]:
195
+ """Select and reconcile the etag header pair for an operation.
196
+
197
+ When multiple etag-typed headers are present, prefer the standard
198
+ If-Match / If-None-Match pair. Synthesize a missing partner when only
199
+ one side is present, and strip etagRole from non-selected candidates.
200
+
201
+ Returns (property_if_match, property_if_none_match) — both None when
202
+ there are no etag candidates.
203
+ """
204
+ property_if_match = _pick_etag_slot(if_match_candidates, STANDARD_IF_MATCH_WIRE_NAME)
205
+ property_if_none_match = _pick_etag_slot(if_none_match_candidates, STANDARD_IF_NONE_MATCH_WIRE_NAME)
206
+
207
+ # Ensure the promoted pair come from the same family. When one slot is
208
+ # standard and the other custom (cross-family), replace the custom slot
209
+ # with a synthetic standard partner. Also synthesize the missing partner
210
+ # when only one side is present.
211
+ if property_if_match and property_if_none_match:
212
+ match_is_std = get_wire_name_lower(property_if_match) == STANDARD_IF_MATCH_WIRE_NAME
213
+ none_match_is_std = get_wire_name_lower(property_if_none_match) == STANDARD_IF_NONE_MATCH_WIRE_NAME
214
+ if match_is_std and not none_match_is_std:
215
+ property_if_none_match = property_if_match.copy()
216
+ property_if_none_match["wireName"] = STANDARD_IF_NONE_MATCH_WIRE_NAME
217
+ property_if_none_match["etagRole"] = "ifNoneMatch"
218
+ elif none_match_is_std and not match_is_std:
219
+ property_if_match = property_if_none_match.copy()
220
+ property_if_match["wireName"] = STANDARD_IF_MATCH_WIRE_NAME
221
+ property_if_match["etagRole"] = "ifMatch"
222
+ elif not property_if_match and property_if_none_match:
223
+ property_if_match = property_if_none_match.copy()
224
+ property_if_match["wireName"] = STANDARD_IF_MATCH_WIRE_NAME
225
+ property_if_match["etagRole"] = "ifMatch"
226
+ elif property_if_match and not property_if_none_match:
227
+ property_if_none_match = property_if_match.copy()
228
+ property_if_none_match["wireName"] = STANDARD_IF_NONE_MATCH_WIRE_NAME
229
+ property_if_none_match["etagRole"] = "ifNoneMatch"
230
+
231
+ for c in if_match_candidates:
232
+ if c is not property_if_match:
233
+ c.pop("etagRole", None)
234
+ for c in if_none_match_candidates:
235
+ if c is not property_if_none_match:
236
+ c.pop("etagRole", None)
237
+
238
+ return property_if_match, property_if_none_match
239
+
240
+
241
+ def _process_operation_etag_headers(
242
+ operation: dict[str, Any],
243
+ client: dict[str, Any],
244
+ version_tolerant: bool,
245
+ ) -> None:
246
+ """Collect etag candidates from *operation*, resolve the promoted pair,
247
+ and update *operation* / *client* accordingly."""
248
+ if_match_candidates: list[dict[str, Any]] = []
249
+ if_none_match_candidates: list[dict[str, Any]] = []
250
+ for p in operation["parameters"]:
251
+ wire_name_lower = get_wire_name_lower(p)
252
+ if p["location"] == "header" and wire_name_lower == "client-request-id":
253
+ client["requestIdHeaderName"] = wire_name_lower
254
+ if version_tolerant and p["location"] == "header":
255
+ role = _get_etag_role(p)
256
+ if role == "ifMatch":
257
+ if_match_candidates.append(p)
258
+ elif role == "ifNoneMatch":
259
+ if_none_match_candidates.append(p)
260
+
261
+ property_if_match, property_if_none_match = _resolve_etag_pair(
262
+ if_match_candidates, if_none_match_candidates
263
+ )
264
+
265
+ if property_if_match and property_if_none_match:
266
+ etag_params = {id(property_if_match), id(property_if_none_match)}
267
+ operation["parameters"] = [
268
+ item for item in operation["parameters"] if id(item) not in etag_params
269
+ ] + [property_if_match, property_if_none_match]
270
+ operation["hasEtag"] = True
271
+ client["hasEtag"] = True
272
+
273
+
172
274
  def headers_convert(yaml_data: dict[str, Any], replace_data: Any) -> None:
173
275
  if isinstance(replace_data, dict):
174
276
  for k, v in replace_data.items():
@@ -313,40 +415,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
313
415
  yaml_data["builderPadName"] = to_snake_case(prop_name)
314
416
  for og in yaml_data.get("operationGroups", []):
315
417
  for o in og["operations"]:
316
- property_if_match = None
317
- property_if_none_match = None
318
- for p in o["parameters"]:
319
- wire_name_lower = get_wire_name_lower(p)
320
- if p["location"] == "header" and wire_name_lower == "client-request-id":
321
- yaml_data["requestIdHeaderName"] = wire_name_lower
322
- if self.version_tolerant and p["location"] == "header":
323
- role = _get_etag_role(p)
324
- if role == "ifMatch" and not property_if_match:
325
- property_if_match = p
326
- elif role == "ifNoneMatch" and not property_if_none_match:
327
- property_if_none_match = p
328
- # pylint: disable=line-too-long
329
- # some service(e.g. https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cosmos-db/data-plane/Microsoft.Tables/preview/2019-02-02/table.json)
330
- # only has one, so we need to add "if-none-match" or "if-match" if it's missing
331
- if not property_if_match and property_if_none_match:
332
- property_if_match = property_if_none_match.copy()
333
- property_if_match["wireName"] = "if-match"
334
- property_if_match["etagRole"] = "ifMatch"
335
- if not property_if_none_match and property_if_match:
336
- property_if_none_match = property_if_match.copy()
337
- property_if_none_match["wireName"] = "if-none-match"
338
- property_if_none_match["etagRole"] = "ifNoneMatch"
339
-
340
- if property_if_match and property_if_none_match:
341
- # arrange if-match and if-none-match to the end of parameters
342
- etag_params = {id(property_if_match), id(property_if_none_match)}
343
- o["parameters"] = [item for item in o["parameters"] if id(item) not in etag_params] + [
344
- property_if_match,
345
- property_if_none_match,
346
- ]
347
-
348
- o["hasEtag"] = True
349
- yaml_data["hasEtag"] = True
418
+ _process_operation_etag_headers(o, yaml_data, self.version_tolerant)
350
419
 
351
420
  # add client signature cloud_setting for arm
352
421
  if self.azure_arm and yaml_data["parameters"]:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typespec/http-client-python",
3
- "version": "0.30.0-alpha.20260529.2",
3
+ "version": "0.30.0-alpha.20260601.2",
4
4
  "author": "Microsoft Corporation",
5
5
  "description": "TypeSpec emitter for Python SDKs",
6
6
  "homepage": "https://typespec.io",