@typespec/http-client-python 0.30.0-alpha.20260601.1 → 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.
@@ -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"]:
@@ -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.20260601.1",
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",