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

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 %}
@@ -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 %}
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.1",
4
4
  "author": "Microsoft Corporation",
5
5
  "description": "TypeSpec emitter for Python SDKs",
6
6
  "homepage": "https://typespec.io",