Skip to content

Commit 80e4cc2

Browse files
authored
Merge branch 'main' into feat/J-1867-request-message-size-limits
2 parents 9570cc1 + f139a67 commit 80e4cc2

12 files changed

Lines changed: 760 additions & 413 deletions

client-sdks/openapi/patch_hierarchy.py

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,13 @@ def patch_optional_import(api_file: Path) -> bool:
152152

153153

154154
def patch_ogx_client(client_file: Path, pairs: list[tuple[str, str]]) -> bool:
155-
"""Patch OgxClient to wire up parent-child API relationships.
155+
"""Patch OgxClient and AsyncOgxClient to wire up parent-child API relationships.
156156
157-
Looks for the '# Nested API structure' comment and inserts assignments like:
157+
Looks for all '# Nested API structure' comment anchors and inserts assignments like:
158158
self.chat.completions = self.completions
159159
160+
This handles both OgxClient and AsyncOgxClient in the same file.
161+
160162
Returns True if the file was modified.
161163
"""
162164
if not client_file.exists():
@@ -166,14 +168,13 @@ def patch_ogx_client(client_file: Path, pairs: list[tuple[str, str]]) -> bool:
166168
with open(client_file) as f:
167169
lines = f.readlines()
168170

169-
# Find the anchor comment
170-
comment_idx = None
171+
# Find all anchor comment indices
172+
comment_indices = []
171173
for i, line in enumerate(lines):
172174
if "# Nested API structure" in line:
173-
comment_idx = i
174-
break
175+
comment_indices.append(i)
175176

176-
if comment_idx is None:
177+
if not comment_indices:
177178
print(f" Warning: '# Nested API structure' comment not found in {client_file}")
178179
return False
179180

@@ -185,30 +186,32 @@ def patch_ogx_client(client_file: Path, pairs: list[tuple[str, str]]) -> bool:
185186
if any(test_line in line for line in lines):
186187
return False
187188

188-
comment_line = lines[comment_idx]
189-
indent = len(comment_line) - len(comment_line.lstrip())
190-
191-
patch_lines = [f"{' ' * indent}# Wire up parent-child API relationships\n"]
192-
for parent_tag, child_tag in pairs:
193-
parent_snake = to_snake_case(parent_tag)
194-
child_snake = to_snake_case(child_tag)
195-
patch_lines.append(f"{' ' * indent}self.{parent_snake}.{child_snake} = self.{child_snake}\n")
196-
# Also add a short alias if the child name is prefixed with the parent name
197-
# e.g., chat_completions -> chat.completions
198-
if child_snake.startswith(f"{parent_snake}_"):
199-
subresource_name = child_snake.removeprefix(f"{parent_snake}_")
200-
patch_lines.append(
201-
f"{' ' * indent}self.{parent_snake}.__dict__['{subresource_name}'] = self.{child_snake}\n"
202-
)
203-
204-
insert_idx = comment_idx + 1
205-
for line in reversed(patch_lines):
206-
lines.insert(insert_idx, line)
189+
# Patch each anchor (process in reverse so line indices remain valid)
190+
for comment_idx in reversed(comment_indices):
191+
comment_line = lines[comment_idx]
192+
indent = len(comment_line) - len(comment_line.lstrip())
193+
194+
patch_lines = [f"{' ' * indent}# Wire up parent-child API relationships\n"]
195+
for parent_tag, child_tag in pairs:
196+
parent_snake = to_snake_case(parent_tag)
197+
child_snake = to_snake_case(child_tag)
198+
patch_lines.append(f"{' ' * indent}self.{parent_snake}.{child_snake} = self.{child_snake}\n")
199+
# Also add a short alias if the child name is prefixed with the parent name
200+
# e.g., chat_completions -> chat.completions
201+
if child_snake.startswith(f"{parent_snake}_"):
202+
subresource_name = child_snake.removeprefix(f"{parent_snake}_")
203+
patch_lines.append(
204+
f"{' ' * indent}self.{parent_snake}.__dict__['{subresource_name}'] = self.{child_snake}\n"
205+
)
206+
207+
insert_idx = comment_idx + 1
208+
for line in reversed(patch_lines):
209+
lines.insert(insert_idx, line)
207210

208211
with open(client_file, "w") as f:
209212
f.writelines(lines)
210213

211-
print(f" Patched OgxClient with {len(pairs)} parent-child relationships")
214+
print(f" Patched OgxClient and AsyncOgxClient with {len(pairs)} parent-child relationships each")
212215
return True
213216

214217

client-sdks/openapi/templates/python/__init__package.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ __all__ = [
2929
"OgxClient",
3030
"AsyncOgxClient",
3131
{{#apiInfo}}{{#apis}}"{{classname}}",
32+
"Async{{classname}}",
3233
{{/apis}}{{/apiInfo}}"ApiResponse",
3334
"APIResponse",
3435
"ApiClient",

client-sdks/openapi/templates/python/api.mustache

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ from typing_extensions import Annotated
1818
{{/imports}}
1919

2020
from {{packageName}}.api_client import ApiClient, RequestSerialized
21+
from {{packageName}}.async_api_client import AsyncApiClient
2122
from {{packageName}}.api_response import ApiResponse
2223
from {{packageName}}.rest import RESTResponseType
2324
from {{packageName}}.stream import Stream
25+
from {{packageName}}.async_stream import AsyncStream
2426
from {{packageName}}.lib._utils import pascal_to_snake_case
2527

2628

@@ -339,5 +341,148 @@ class {{classname}}:
339341
{{alias}} = {{target}}
340342
{{/vendorExtensions.x-operation-aliases}}
341343

344+
{{/operation}}
345+
346+
347+
class Async{{classname}}({{classname}}):
348+
"""Async version of {{classname}}.
349+
350+
Inherits serialization and deserialization from {{classname}}.
351+
Overrides HTTP methods to use async/await with AsyncApiClient.
352+
"""
353+
354+
def __init__(self, api_client=None) -> None:
355+
if api_client is None:
356+
api_client = AsyncApiClient.get_default()
357+
self.api_client = api_client
358+
self.logger = logging.getLogger(Async{{classname}}.__name__)
359+
360+
# Child API attributes (set by AsyncOgxClient based on x-nesting-path)
361+
{{#operation}}{{#vendorExtensions.x-nesting-path}}{{#-first}}{{#tags}}{{#-first}}{{#vendorExtensions.x-nesting-path}}{{^-last}} self.{{.}}: Any | None = None # Next in nesting path
362+
{{/-last}}{{/vendorExtensions.x-nesting-path}}{{/-first}}{{/tags}}{{/-first}}{{/vendorExtensions.x-nesting-path}}{{/operation}}
363+
364+
def _create_event_stream(self, response_data: RESTResponseType, stream_type_name: str) -> AsyncStream[Any]:
365+
"""Create a typed async event stream from an SSE response.
366+
367+
Args:
368+
response_data: The raw HTTP response containing the SSE stream.
369+
stream_type_name: The schema name for the streaming response type.
370+
"""
371+
stream_class = None
372+
discriminator_map = None
373+
discriminator_property = None
374+
try:
375+
module_name = pascal_to_snake_case(stream_type_name)
376+
model_module = importlib.import_module('{{packageName}}.models.' + module_name)
377+
stream_class = getattr(model_module, stream_type_name)
378+
discriminator_map = getattr(stream_class, 'discriminator_value_class_map', None)
379+
discriminator_property = getattr(stream_class, 'discriminator_property_name', None)
380+
except (ImportError, ModuleNotFoundError, AttributeError) as e:
381+
self.logger.debug(f"Could not import stream type {stream_type_name}: {e}")
382+
383+
def stream_decoder(data_str: str) -> Any:
384+
if not data_str:
385+
return None
386+
try:
387+
data = json.loads(data_str)
388+
except json.JSONDecodeError:
389+
return data_str
390+
391+
if discriminator_map and discriminator_property:
392+
disc_value = data.get(discriminator_property, '')
393+
variant_class_name = discriminator_map.get(disc_value)
394+
if variant_class_name:
395+
try:
396+
variant_module_name = pascal_to_snake_case(variant_class_name)
397+
variant_module = importlib.import_module('{{packageName}}.models.' + variant_module_name)
398+
variant_class = getattr(variant_module, variant_class_name)
399+
return variant_class.from_dict(data)
400+
except Exception as e:
401+
self.logger.debug(f"Failed to deserialize as {variant_class_name}: {e}")
402+
403+
if stream_class is not None:
404+
try:
405+
result = stream_class.from_dict(data)
406+
if hasattr(result, 'actual_instance') and result.actual_instance is not None:
407+
return result.actual_instance
408+
return result
409+
except Exception as e:
410+
self.logger.debug(f"Failed to deserialize as {stream_type_name}: {e}")
411+
412+
return data
413+
414+
return AsyncStream(
415+
response=response_data.response,
416+
client=self.api_client,
417+
decoder=stream_decoder,
418+
)
419+
420+
{{#operation}}
421+
422+
@validate_call
423+
async def {{#vendorExtensions.x-operation-name}}{{vendorExtensions.x-operation-name}}{{/vendorExtensions.x-operation-name}}{{^vendorExtensions.x-operation-name}}{{operationId}}{{/vendorExtensions.x-operation-name}}{{>partial_api_args}} -> {{#vendorExtensions.x-streaming}}{{#vendorExtensions.x-unwrap-list-response}}{{#returnType}}{{#returnContainer}}{{{returnBaseType}}} | AsyncStream[{{{returnBaseType}}}]{{/returnContainer}}{{^returnContainer}}{{{returnType}}} | AsyncStream[{{{returnType}}}]{{/returnContainer}}{{/returnType}}{{^returnType}}None | AsyncStream[None]{{/returnType}}{{/vendorExtensions.x-unwrap-list-response}}{{^vendorExtensions.x-unwrap-list-response}}{{{returnType}}}{{^returnType}}None{{/returnType}} | AsyncStream[{{{returnType}}}{{^returnType}}None{{/returnType}}]{{/vendorExtensions.x-unwrap-list-response}}{{/vendorExtensions.x-streaming}}{{^vendorExtensions.x-streaming}}{{#vendorExtensions.x-unwrap-list-response}}{{#returnType}}{{#returnContainer}}{{{returnBaseType}}}{{/returnContainer}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}}{{/returnType}}{{^returnType}}None{{/returnType}}{{/vendorExtensions.x-unwrap-list-response}}{{^vendorExtensions.x-unwrap-list-response}}{{{returnType}}}{{^returnType}}None{{/returnType}}{{/vendorExtensions.x-unwrap-list-response}}{{/vendorExtensions.x-streaming}}:
424+
{{>partial_api}}
425+
426+
response_data = await self.api_client.call_api(
427+
*_param,
428+
_request_timeout=_request_timeout
429+
)
430+
431+
{{#vendorExtensions.x-streaming}}
432+
# Check if this is a streaming response
433+
content_type = response_data.response.headers.get('Content-Type', '')
434+
if 'text/event-stream' in content_type:
435+
return self._create_event_stream(response_data, "{{vendorExtensions.x-streaming-type}}")
436+
437+
{{/vendorExtensions.x-streaming}}
438+
await response_data.aread()
439+
_deserialized = self.api_client.response_deserialize(
440+
response_data=response_data,
441+
response_types_map=_response_types_map,
442+
).data
443+
# Unwrap List*Response wrappers to return the data field directly
444+
{{#vendorExtensions.x-unwrap-list-response}}
445+
if _deserialized is not None and hasattr(_deserialized, 'data'):
446+
return _deserialized.data
447+
{{/vendorExtensions.x-unwrap-list-response}}
448+
return _deserialized
449+
450+
@validate_call
451+
async def {{#vendorExtensions.x-operation-name}}{{vendorExtensions.x-operation-name}}{{/vendorExtensions.x-operation-name}}{{^vendorExtensions.x-operation-name}}{{operationId}}{{/vendorExtensions.x-operation-name}}_with_http_info{{>partial_api_args}} -> {{#vendorExtensions.x-streaming}}ApiResponse[{{{returnType}}}{{^returnType}}None{{/returnType}}] | AsyncStream[{{{returnType}}}{{^returnType}}None{{/returnType}}]{{/vendorExtensions.x-streaming}}{{^vendorExtensions.x-streaming}}ApiResponse[{{{returnType}}}{{^returnType}}None{{/returnType}}]{{/vendorExtensions.x-streaming}}:
452+
{{>partial_api}}
453+
454+
response_data = await self.api_client.call_api(
455+
*_param,
456+
_request_timeout=_request_timeout
457+
)
458+
459+
{{#vendorExtensions.x-streaming}}
460+
# Check if this is a streaming response
461+
content_type = response_data.response.headers.get('Content-Type', '')
462+
if 'text/event-stream' in content_type:
463+
return self._create_event_stream(response_data, "{{vendorExtensions.x-streaming-type}}")
464+
465+
{{/vendorExtensions.x-streaming}}
466+
await response_data.aread()
467+
return self.api_client.response_deserialize(
468+
response_data=response_data,
469+
response_types_map=_response_types_map,
470+
)
471+
472+
@validate_call
473+
async def {{#vendorExtensions.x-operation-name}}{{vendorExtensions.x-operation-name}}{{/vendorExtensions.x-operation-name}}{{^vendorExtensions.x-operation-name}}{{operationId}}{{/vendorExtensions.x-operation-name}}_without_preload_content{{>partial_api_args}} -> RESTResponseType:
474+
{{>partial_api}}
475+
476+
response_data = await self.api_client.call_api(
477+
*_param,
478+
_request_timeout=_request_timeout
479+
)
480+
return response_data.response
481+
482+
483+
{{#vendorExtensions.x-operation-aliases}}
484+
{{alias}} = {{target}}
485+
{{/vendorExtensions.x-operation-aliases}}
486+
342487
{{/operation}}
343488
{{/operations}}

0 commit comments

Comments
 (0)