pydantic-openapi-generator 2.4.0__tar.gz → 2.5.0__tar.gz

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.
Files changed (41) hide show
  1. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/PKG-INFO +1 -1
  2. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/pyproject.toml +1 -1
  3. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/client_generator.py +108 -3
  4. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +64 -22
  5. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +64 -22
  6. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/models.py +10 -1
  7. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/.gitignore +0 -0
  8. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/LICENSE +0 -0
  9. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/README.md +0 -0
  10. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/__init__.py +0 -0
  11. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/__main__.py +0 -0
  12. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/common.py +0 -0
  13. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/__init__.py +0 -0
  14. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/base_parameter.py +0 -0
  15. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/class_var_parameter.py +0 -0
  16. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/function_parameter.py +0 -0
  17. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/generator_config.py +0 -0
  18. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/kwarg_parameter.py +0 -0
  19. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/loader.py +0 -0
  20. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/parameter_source.py +0 -0
  21. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/parameter_union.py +0 -0
  22. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/config/utils.py +0 -0
  23. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/generate_data.py +0 -0
  24. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/__init__.py +0 -0
  25. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/__init__.py +0 -0
  26. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/common.py +0 -0
  27. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/exception_generator.py +0 -0
  28. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/generator.py +0 -0
  29. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/jinja_config.py +0 -0
  30. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/model_generator.py +0 -0
  31. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/alias_union.jinja2 +0 -0
  32. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/discriminator_enum.jinja2 +0 -0
  33. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/enum.jinja2 +0 -0
  34. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/http_exception.jinja2 +0 -0
  35. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/models.jinja2 +0 -0
  36. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/language_converters/python/templates/models_pydantic_2.jinja2 +0 -0
  37. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/parsers/__init__.py +0 -0
  38. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/parsers/openapi_30.py +0 -0
  39. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/parsers/openapi_31.py +0 -0
  40. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/py.typed +0 -0
  41. {pydantic_openapi_generator-2.4.0 → pydantic_openapi_generator-2.5.0}/src/pydantic_openapi_generator/version_detector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pydantic-openapi-generator
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Openapi Python Generator
5
5
  Project-URL: Homepage, https://github.com/mattcoulter7/pydantic-openapi-generator
6
6
  Project-URL: Repository, https://github.com/mattcoulter7/pydantic-openapi-generator
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic-openapi-generator"
3
- version = "2.4.0"
3
+ version = "2.5.0"
4
4
  description = "Openapi Python Generator"
5
5
  authors = [
6
6
  { name = "Marco Müllner", email = "muellnermarco@gmail.com" },
@@ -58,6 +58,7 @@ from pydantic_openapi_generator.models import (
58
58
  Model,
59
59
  OpReturnType,
60
60
  RequestBodyDefinition,
61
+ ResponseContentHandler,
61
62
  ResponseVariant,
62
63
  ServiceOperation,
63
64
  TypeConversion,
@@ -154,6 +155,71 @@ def operation_is_sse(op: Operation) -> bool:
154
155
  return False
155
156
 
156
157
 
158
+ def _sse_data_schema_from_media_type(media_type: Any) -> Any:
159
+ media_type_schema = getattr(media_type, "media_type_schema", None)
160
+ if media_type_schema is None:
161
+ return None
162
+
163
+ if is_reference_type(media_type_schema):
164
+ return media_type_schema
165
+
166
+ if not is_schema_type(media_type_schema):
167
+ return None
168
+
169
+ properties = getattr(media_type_schema, "properties", None)
170
+ if isinstance(properties, dict) and "data" in properties:
171
+ return properties["data"]
172
+
173
+ schema_type = getattr(media_type_schema, "type", None)
174
+ if schema_type != "object" and str(schema_type) != "DataType.OBJECT":
175
+ return media_type_schema
176
+
177
+ return None
178
+
179
+
180
+ def generate_sse_data_handler(operation: Operation) -> Optional[ResponseContentHandler]:
181
+ if not getattr(operation, "responses", None):
182
+ return None
183
+
184
+ for status_code, response in operation.responses.items():
185
+ try:
186
+ if not str(status_code).startswith("2"):
187
+ continue
188
+ except Exception as e:
189
+ logger = logging.getLogger(__name__)
190
+ logger.debug("Skipping response status key; conversion failed", exc_info=e)
191
+ continue
192
+
193
+ if not is_response_type(response):
194
+ continue
195
+
196
+ content = getattr(response, "content", None)
197
+ if not isinstance(content, dict):
198
+ continue
199
+
200
+ media_type = content.get("text/event-stream")
201
+ if not is_media_type(media_type):
202
+ continue
203
+
204
+ data_schema = _sse_data_schema_from_media_type(media_type)
205
+ if data_schema is None:
206
+ continue
207
+
208
+ variant = _response_variant_from_schema(int(status_code), "application/json", data_schema)
209
+ if variant.type is None or variant.body_kind == "empty":
210
+ continue
211
+
212
+ return ResponseContentHandler(
213
+ content_type="text/event-stream",
214
+ type=variant.type,
215
+ complex_type=variant.complex_type,
216
+ list_type=variant.list_type,
217
+ body_kind=variant.body_kind, # type: ignore[arg-type]
218
+ )
219
+
220
+ return None
221
+
222
+
157
223
  HTTP_OPERATIONS = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
158
224
 
159
225
 
@@ -620,9 +686,47 @@ def _return_type_hint(variants: List[ResponseVariant]) -> str:
620
686
  return hints[0] if len(hints) == 1 else " | ".join(hints)
621
687
 
622
688
 
689
+ def _variant_deserialization_key(variant: ResponseVariant) -> tuple:
690
+ return (
691
+ variant.body_kind,
692
+ variant.type.converted_type if variant.type is not None else None,
693
+ variant.complex_type,
694
+ variant.list_type,
695
+ )
696
+
697
+
698
+ def _unambiguous_content_handlers(
699
+ variants: List[ResponseVariant],
700
+ ) -> List[ResponseContentHandler]:
701
+ variants_by_content_type: Dict[str, List[ResponseVariant]] = {}
702
+ for variant in variants:
703
+ if variant.content_type is None or variant.body_kind == "empty":
704
+ continue
705
+ variants_by_content_type.setdefault(variant.content_type.lower(), []).append(variant)
706
+
707
+ handlers: List[ResponseContentHandler] = []
708
+ for content_type, content_variants in variants_by_content_type.items():
709
+ keys = {_variant_deserialization_key(variant) for variant in content_variants}
710
+ if len(keys) != 1:
711
+ continue
712
+
713
+ variant = content_variants[0]
714
+ handlers.append(
715
+ ResponseContentHandler(
716
+ content_type=content_type,
717
+ type=variant.type,
718
+ complex_type=variant.complex_type,
719
+ list_type=variant.list_type,
720
+ body_kind=variant.body_kind, # type: ignore[arg-type]
721
+ )
722
+ )
723
+
724
+ return handlers
725
+
726
+
623
727
  def generate_return_type(operation: Operation) -> OpReturnType:
624
728
  if operation.responses is None:
625
- return OpReturnType(type=None, status_code=200, complex_type=False, accepted_status_codes=[200])
729
+ return OpReturnType(type=None, status_code=200, complex_type=False)
626
730
 
627
731
  good_responses: List[Tuple[int, Union[Response, Reference]]] = [
628
732
  (int(status_code), response)
@@ -630,7 +734,7 @@ def generate_return_type(operation: Operation) -> OpReturnType:
630
734
  if status_code.startswith("2")
631
735
  ]
632
736
  if len(good_responses) == 0:
633
- return OpReturnType(type=None, status_code=200, complex_type=False, accepted_status_codes=[200])
737
+ return OpReturnType(type=None, status_code=200, complex_type=False)
634
738
 
635
739
  variants: List[ResponseVariant] = []
636
740
  for status_code, response in good_responses:
@@ -643,8 +747,8 @@ def generate_return_type(operation: Operation) -> OpReturnType:
643
747
  complex_type=first_variant.complex_type,
644
748
  list_type=first_variant.list_type,
645
749
  variants=variants,
646
- accepted_status_codes=sorted({status_code for status_code, _ in good_responses}),
647
750
  accept_content_types=list(dict.fromkeys(v.content_type for v in variants if v.content_type is not None)),
751
+ unambiguous_content_handlers=_unambiguous_content_handlers(variants),
648
752
  return_type_hint=_return_type_hint(variants),
649
753
  )
650
754
 
@@ -732,6 +836,7 @@ def generate_clients(
732
836
  path_name=path_name,
733
837
  method=http_operation,
734
838
  is_sse=operation_is_sse(op),
839
+ sse_data_handler=generate_sse_data_handler(op),
735
840
  use_orjson=common.get_use_orjson(),
736
841
  )
737
842
 
@@ -10,7 +10,6 @@ import httpx
10
10
  from pydantic import BaseModel, SecretStr, TypeAdapter
11
11
 
12
12
  from ..models import *
13
- from ..exceptions import HTTPException
14
13
 
15
14
  {% set _base_url = servers[0].url if servers|length > 0 else "/" %}
16
15
 
@@ -54,7 +53,7 @@ class AsyncClient(BaseModel):
54
53
  {% endfor %}
55
54
  {% for op in operations %}
56
55
  async def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> {%- if op.is_sse -%}
57
- AsyncGenerator[str | dict[str, Any], None]
56
+ AsyncGenerator[str | {% if op.sse_data_handler and op.sse_data_handler.type %}{% if op.sse_data_handler.list_type %}list[{{ op.sse_data_handler.list_type }}]{% else %}{{ op.sse_data_handler.type.converted_type }}{% endif %}{% else %}dict[str, Any]{% endif %}, None]
58
57
  {%- else -%}
59
58
  {{ op.return_type.return_type_hint or "None" }}
60
59
  {%- endif -%}:
@@ -121,11 +120,7 @@ AsyncGenerator[str | dict[str, Any], None]
121
120
  {% endif %}
122
121
  {% endif %}
123
122
  ) as response:
124
- if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
125
- raise HTTPException(
126
- response.status_code,
127
- f"{{ op.operation_id }} failed with status code: {response.status_code}",
128
- )
123
+ response.raise_for_status()
129
124
 
130
125
  async for line in response.aiter_lines():
131
126
  if not line:
@@ -137,11 +132,16 @@ AsyncGenerator[str | dict[str, Any], None]
137
132
  if payload == "[DONE]":
138
133
  break
139
134
  try:
135
+ {% if op.sse_data_handler and op.sse_data_handler.type %}
136
+ obj = json.loads(payload)
137
+ yield TypeAdapter({% if op.sse_data_handler.list_type %}list[{{ op.sse_data_handler.list_type }}]{% else %}{{ op.sse_data_handler.type.converted_type }}{% endif %}).validate_python(obj)
138
+ {% else %}
140
139
  obj = json.loads(payload)
141
140
  if isinstance(obj, dict):
142
141
  yield obj
143
142
  else:
144
143
  yield payload
144
+ {% endif %}
145
145
  except Exception:
146
146
  yield payload
147
147
  else:
@@ -169,23 +169,60 @@ AsyncGenerator[str | dict[str, Any], None]
169
169
  {% endif %}
170
170
  )
171
171
 
172
- if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
173
- raise HTTPException(
174
- response.status_code,
175
- f"{{ op.operation_id }} failed with status code: {response.status_code}",
176
- )
172
+ response.raise_for_status()
173
+
174
+ {% set empty_variants = op.return_type.variants | selectattr("body_kind", "equalto", "empty") | list %}
175
+ {% if empty_variants|length > 0 %}
176
+ if not response.content:
177
+ return None
178
+
179
+ {% endif %}
180
+ {% if op.return_type.unambiguous_content_handlers|length == 1 %}
181
+ {% set handler = op.return_type.unambiguous_content_handlers[0] %}
182
+ {% if handler.body_kind == "binary" %}
183
+ return response.content
184
+ {% elif handler.body_kind == "text" %}
185
+ return response.text
186
+ {% elif handler.type is not none and handler.type.converted_type is not none %}
187
+ {% if handler.list_type is none %}
188
+ return TypeAdapter({{ handler.type.converted_type }}).validate_python(response.json())
189
+ {% else %}
190
+ return TypeAdapter(list[{{ handler.list_type }}]).validate_python(response.json())
191
+ {% endif %}
192
+ {% else %}
193
+ return response.json()
194
+ {% endif %}
195
+ {% else %}
196
+ if not response.content:
197
+ return None
177
198
 
178
199
  _response_content_type = response.headers.get("content-type", "").split(";", 1)[0].lower()
200
+ {% set unambiguous_content_types = op.return_type.unambiguous_content_handlers | map(attribute="content_type") | list %}
201
+ {% for handler in op.return_type.unambiguous_content_handlers %}
202
+ if _response_content_type == "{{ handler.content_type }}":
203
+ {% if handler.body_kind == "binary" %}
204
+ return response.content
205
+ {% elif handler.body_kind == "text" %}
206
+ return response.text
207
+ {% elif handler.type is not none and handler.type.converted_type is not none %}
208
+ {% if handler.list_type is none %}
209
+ return TypeAdapter({{ handler.type.converted_type }}).validate_python(response.json())
210
+ {% else %}
211
+ return TypeAdapter(list[{{ handler.list_type }}]).validate_python(response.json())
212
+ {% endif %}
213
+ {% else %}
214
+ return response.json()
215
+ {% endif %}
216
+
217
+ {% endfor %}
179
218
  {% for variant in op.return_type.variants %}
180
- {% set same_status_variants = op.return_type.variants | selectattr("status_code", "equalto", variant.status_code) | list %}
181
- if response.status_code == {{ variant.status_code }}{% if same_status_variants|length > 1 and variant.content_type %} and _response_content_type == "{{ variant.content_type }}"{% endif %}:
182
- {% if variant.body_kind == "empty" or variant.type is none or variant.type.converted_type is none %}
183
- return None
184
- {% elif variant.body_kind == "binary" %}
219
+ {% if variant.content_type and variant.body_kind != "empty" and variant.content_type not in unambiguous_content_types %}
220
+ if response.status_code == {{ variant.status_code }} and _response_content_type == "{{ variant.content_type }}":
221
+ {% if variant.body_kind == "binary" %}
185
222
  return response.content
186
223
  {% elif variant.body_kind == "text" %}
187
224
  return response.text
188
- {% elif variant.complex_type %}
225
+ {% elif variant.type is not none and variant.type.converted_type is not none %}
189
226
  {% if variant.list_type is none %}
190
227
  return TypeAdapter({{ variant.type.converted_type }}).validate_python(response.json())
191
228
  {% else %}
@@ -195,11 +232,16 @@ AsyncGenerator[str | dict[str, Any], None]
195
232
  return response.json()
196
233
  {% endif %}
197
234
 
235
+ {% endif %}
198
236
  {% endfor %}
199
- raise HTTPException(
200
- response.status_code,
201
- f"{{ op.operation_id }} failed with unsupported content type: {_response_content_type}",
202
- )
237
+ if _response_content_type == "application/json" or _response_content_type.endswith("+json"):
238
+ return response.json()
239
+
240
+ if _response_content_type.startswith("text/"):
241
+ return response.text
242
+
243
+ return response.content
244
+ {% endif %}
203
245
  {% endif %}
204
246
 
205
247
  {% endfor %}
@@ -10,7 +10,6 @@ import httpx
10
10
  from pydantic import BaseModel, SecretStr, TypeAdapter
11
11
 
12
12
  from ..models import *
13
- from ..exceptions import HTTPException
14
13
 
15
14
  {% set _base_url = servers[0].url if servers|length > 0 else "/" %}
16
15
 
@@ -54,7 +53,7 @@ class SyncClient(BaseModel):
54
53
  {% endfor %}
55
54
  {% for op in operations %}
56
55
  def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> {%- if op.is_sse -%}
57
- Generator[str | dict[str, Any], None, None]
56
+ Generator[str | {% if op.sse_data_handler and op.sse_data_handler.type %}{% if op.sse_data_handler.list_type %}list[{{ op.sse_data_handler.list_type }}]{% else %}{{ op.sse_data_handler.type.converted_type }}{% endif %}{% else %}dict[str, Any]{% endif %}, None, None]
58
57
  {%- else -%}
59
58
  {{ op.return_type.return_type_hint or "None" }}
60
59
  {%- endif -%}:
@@ -121,11 +120,7 @@ Generator[str | dict[str, Any], None, None]
121
120
  {% endif %}
122
121
  {% endif %}
123
122
  ) as response:
124
- if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
125
- raise HTTPException(
126
- response.status_code,
127
- f"{{ op.operation_id }} failed with status code: {response.status_code}",
128
- )
123
+ response.raise_for_status()
129
124
 
130
125
  for line in response.iter_lines():
131
126
  if not line:
@@ -137,11 +132,16 @@ Generator[str | dict[str, Any], None, None]
137
132
  if payload == "[DONE]":
138
133
  break
139
134
  try:
135
+ {% if op.sse_data_handler and op.sse_data_handler.type %}
136
+ obj = json.loads(payload)
137
+ yield TypeAdapter({% if op.sse_data_handler.list_type %}list[{{ op.sse_data_handler.list_type }}]{% else %}{{ op.sse_data_handler.type.converted_type }}{% endif %}).validate_python(obj)
138
+ {% else %}
140
139
  obj = json.loads(payload)
141
140
  if isinstance(obj, dict):
142
141
  yield obj
143
142
  else:
144
143
  yield payload
144
+ {% endif %}
145
145
  except Exception:
146
146
  yield payload
147
147
  else:
@@ -168,23 +168,60 @@ Generator[str | dict[str, Any], None, None]
168
168
  {% endif %}
169
169
  )
170
170
 
171
- if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
172
- raise HTTPException(
173
- response.status_code,
174
- f"{{ op.operation_id }} failed with status code: {response.status_code}",
175
- )
171
+ response.raise_for_status()
172
+
173
+ {% set empty_variants = op.return_type.variants | selectattr("body_kind", "equalto", "empty") | list %}
174
+ {% if empty_variants|length > 0 %}
175
+ if not response.content:
176
+ return None
177
+
178
+ {% endif %}
179
+ {% if op.return_type.unambiguous_content_handlers|length == 1 %}
180
+ {% set handler = op.return_type.unambiguous_content_handlers[0] %}
181
+ {% if handler.body_kind == "binary" %}
182
+ return response.content
183
+ {% elif handler.body_kind == "text" %}
184
+ return response.text
185
+ {% elif handler.type is not none and handler.type.converted_type is not none %}
186
+ {% if handler.list_type is none %}
187
+ return TypeAdapter({{ handler.type.converted_type }}).validate_python(response.json())
188
+ {% else %}
189
+ return TypeAdapter(list[{{ handler.list_type }}]).validate_python(response.json())
190
+ {% endif %}
191
+ {% else %}
192
+ return response.json()
193
+ {% endif %}
194
+ {% else %}
195
+ if not response.content:
196
+ return None
176
197
 
177
198
  _response_content_type = response.headers.get("content-type", "").split(";", 1)[0].lower()
199
+ {% set unambiguous_content_types = op.return_type.unambiguous_content_handlers | map(attribute="content_type") | list %}
200
+ {% for handler in op.return_type.unambiguous_content_handlers %}
201
+ if _response_content_type == "{{ handler.content_type }}":
202
+ {% if handler.body_kind == "binary" %}
203
+ return response.content
204
+ {% elif handler.body_kind == "text" %}
205
+ return response.text
206
+ {% elif handler.type is not none and handler.type.converted_type is not none %}
207
+ {% if handler.list_type is none %}
208
+ return TypeAdapter({{ handler.type.converted_type }}).validate_python(response.json())
209
+ {% else %}
210
+ return TypeAdapter(list[{{ handler.list_type }}]).validate_python(response.json())
211
+ {% endif %}
212
+ {% else %}
213
+ return response.json()
214
+ {% endif %}
215
+
216
+ {% endfor %}
178
217
  {% for variant in op.return_type.variants %}
179
- {% set same_status_variants = op.return_type.variants | selectattr("status_code", "equalto", variant.status_code) | list %}
180
- if response.status_code == {{ variant.status_code }}{% if same_status_variants|length > 1 and variant.content_type %} and _response_content_type == "{{ variant.content_type }}"{% endif %}:
181
- {% if variant.body_kind == "empty" or variant.type is none or variant.type.converted_type is none %}
182
- return None
183
- {% elif variant.body_kind == "binary" %}
218
+ {% if variant.content_type and variant.body_kind != "empty" and variant.content_type not in unambiguous_content_types %}
219
+ if response.status_code == {{ variant.status_code }} and _response_content_type == "{{ variant.content_type }}":
220
+ {% if variant.body_kind == "binary" %}
184
221
  return response.content
185
222
  {% elif variant.body_kind == "text" %}
186
223
  return response.text
187
- {% elif variant.complex_type %}
224
+ {% elif variant.type is not none and variant.type.converted_type is not none %}
188
225
  {% if variant.list_type is none %}
189
226
  return TypeAdapter({{ variant.type.converted_type }}).validate_python(response.json())
190
227
  {% else %}
@@ -194,11 +231,16 @@ Generator[str | dict[str, Any], None, None]
194
231
  return response.json()
195
232
  {% endif %}
196
233
 
234
+ {% endif %}
197
235
  {% endfor %}
198
- raise HTTPException(
199
- response.status_code,
200
- f"{{ op.operation_id }} failed with unsupported content type: {_response_content_type}",
201
- )
236
+ if _response_content_type == "application/json" or _response_content_type.endswith("+json"):
237
+ return response.json()
238
+
239
+ if _response_content_type.startswith("text/"):
240
+ return response.text
241
+
242
+ return response.content
243
+ {% endif %}
202
244
  {% endif %}
203
245
 
204
246
  {% endfor %}
@@ -48,8 +48,8 @@ class OpReturnType(BaseModel):
48
48
  complex_type: bool = False
49
49
  list_type: Optional[str] = None
50
50
  variants: List["ResponseVariant"] = Field(default_factory=list)
51
- accepted_status_codes: List[int] = Field(default_factory=list)
52
51
  accept_content_types: List[str] = Field(default_factory=list)
52
+ unambiguous_content_handlers: List["ResponseContentHandler"] = Field(default_factory=list)
53
53
  return_type_hint: Optional[str] = None
54
54
 
55
55
 
@@ -62,6 +62,14 @@ class ResponseVariant(BaseModel):
62
62
  body_kind: Literal["empty", "json", "text", "binary"] = "empty"
63
63
 
64
64
 
65
+ class ResponseContentHandler(BaseModel):
66
+ content_type: str
67
+ type: Optional[TypeConversion] = None
68
+ complex_type: bool = False
69
+ list_type: Optional[str] = None
70
+ body_kind: Literal["json", "text", "binary"]
71
+
72
+
65
73
  class RequestBodyDefinition(BaseModel):
66
74
  content_type: Optional[str] = None
67
75
  encoding: Literal["json", "form", "multipart", "binary", "text"]
@@ -102,6 +110,7 @@ class ServiceOperation(BaseModel):
102
110
  request_body: Optional[RequestBodyDefinition] = None
103
111
  method: str
104
112
  is_sse: bool = False
113
+ sse_data_handler: Optional[ResponseContentHandler] = None
105
114
  use_orjson: bool = False
106
115
 
107
116