diff --git a/codegen-output/my-handwritten-json-model/README.md b/codegen-output/my-handwritten-json-model/README.md new file mode 100644 index 000000000..5a1e0a519 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/README.md @@ -0,0 +1,13 @@ + + +## My Handwritten JSON Model Client + +my_handwritten_json_model client + +### Documentation + +The rpcv2Json counterpart of +smithy.python.tests.handwritten#MyHandwrittenModel, used to drive the +duplex event-stream integration test over rpcv2Json. Shapes are local +copies (not shared with the CBOR service) so each service's shape +closure, default namespace, and generated client stay independent. diff --git a/codegen-output/my-handwritten-json-model/pyproject.toml b/codegen-output/my-handwritten-json-model/pyproject.toml new file mode 100644 index 000000000..d32fb8be3 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/pyproject.toml @@ -0,0 +1,71 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +[project] +name = "my_handwritten_json_model" +version = "0.0.1" +description = "my_handwritten_json_model client" +readme = "README.md" +requires-python = ">=3.12" +keywords = ["smithy", "my_handwritten_json_model"] +license = {text = "Apache-2.0"} +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15" +] + +dependencies = [ + "smithy_aws_core[eventstream]~=0.13.0", + "smithy_core~=0.10.0", + "smithy_http[json, aiohttp, urllib3]~=0.7.0" +] + +[project.optional-dependencies] +awscrt = [ + "smithy_http[awscrt]~=0.7.0" +] + +[dependency-groups] +test = [ + "pytest>=9.0.3,<10.0.0", + "pytest-asyncio>=1.3.0,<1.4.0", + "smithy_test~=0.2.0" +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pyright] +typeCheckingMode = "strict" +reportPrivateUsage = false +reportUnusedFunction = false +reportUnusedVariable = false +reportUnnecessaryComparison = false +reportUnusedClass = false +enableExperimentalFeatures = true +# models.py is the dynamic runtime; models.pyi carries the typed +# surface for consumers, so don't type-check the implementation body. +exclude = ["**/models.py"] + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +ignore = ["F841"] + +[tool.ruff.format] +skip-magic-trailing-comma = true + +[tool.pytest.ini_options] +python_classes = ["!Test"] +asyncio_mode = "auto" diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/__init__.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/__init__.py new file mode 100644 index 000000000..86f2603af --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/__init__.py @@ -0,0 +1,34 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from importlib import import_module +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .client import MyHandwrittenJsonModel + from .config import Config + from .aio.client import AsyncMyHandwrittenJsonModel + from .aio.config import AsyncConfig + +__all__ = [ + "MyHandwrittenJsonModel", + "Config", + "AsyncMyHandwrittenJsonModel", + "AsyncConfig", +] + +_EXPORTS = { + "MyHandwrittenJsonModel": ".client", + "Config": ".config", + "AsyncMyHandwrittenJsonModel": ".aio.client", + "AsyncConfig": ".aio.config", +} + + +def __getattr__(name: str) -> Any: + if name in _EXPORTS: + return getattr(import_module(_EXPORTS[name], __name__), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__version__: str = "0.0.1" diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/__init__.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/__init__.py new file mode 100644 index 000000000..247be3e3d --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/__init__.py @@ -0,0 +1 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/endpoints.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/endpoints.py new file mode 100644 index 000000000..4845025a4 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/endpoints.py @@ -0,0 +1,31 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +"""Endpoint BDD for smithy.python.tests.handwritten.json#MyHandwrittenJsonModel.""" + +from smithy_core.endpoints import EndpointBdd + +# fmt: off +# Tags: ref=0, attr=1, call=2, list=3, map=4, error=5, endpoint=6. +BDD = EndpointBdd( + # (name, type, required, default, builtin, client_context_attribute) + parameters=( + ("endpoint", "string", False, None, "SDK::Endpoint", None), + ), + functions=("isSet",), + conditions=( + ((2, "isSet", (0, "endpoint")), None), # 0: isSet(endpoint) + ), + results=( + None, # 0: no match + (6, (0, "endpoint"), None, ()), # 1 + (5, "(default endpointRuleSet) endpoint is not set - you must configure an endpoint."), # 2 + ), + root=2, + # (condition, high, low); 1 and -1 are terminals, a negative reference is a + # complement edge, and 100_000_000 + k is result k. + nodes=( + -1, 1, -1, + 0, 100_000_001, 100_000_002, + ), +) +# fmt: on diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/schemas.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/schemas.py new file mode 100644 index 000000000..c66421d04 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/_private/schemas.py @@ -0,0 +1,111 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from types import MappingProxyType + +from smithy_core._schema_compact import StaticOperationSchema, StaticSchema + + +n0 = "smithy.python.tests.handwritten.json" +_A = "Alpha" +_i = "id" +_t = "timestamp" +_DSN = "DifferentShapeName" +_n = "name" +_nu = "number" +_IL = "IntegerList" +_m = "member" +_GP = "GammaPayload" +_me = "message" +_v = "values" +_G = "Gamma" +_sN = "sequenceNumber" +_sH = "smithy.api#eventHeader" +_p = "payload" +_sP = "smithy.api#eventPayload" +_SSE = "StreamServiceError" +_s = "smithy.api#error" +_se = "server" +_TE = "ThrottlingEvent" +_c = "client" +_TEr = "TradeEvents" +_sm = "smithy.api#streaming" +_a = "alpha" +_b = "beta" +_g = "gamma" +_d = "delta" +_th = "throttle" +_TESI = "TradeEventStreamInput" +_smi = "smithy.api#input" +_sI = "sessionId" +_e = "events" +_TESO = "TradeEventStreamOutput" +_smit = "smithy.api#output" +_gr = "greeting" +_TES = "TradeEventStream" +_MHJM = "MyHandwrittenJsonModel" +_sJ = "smithy.protocols#rpcv2Json" + +ALPHA: StaticSchema = (18, n0, _A, 0, (_i, _t), ((3, 0), (4, 0))) + +DIFFERENT_SHAPE_NAME: StaticSchema = (18, n0, _DSN, 0, (_n, _nu), ((3, 0), (7, 0))) + +INTEGER_LIST: StaticSchema = (16, n0, _IL, 0, (_m,), ((7, 0),)) + +GAMMA_PAYLOAD: StaticSchema = (18, n0, _GP, 0, (_me, _v), ((3, 0), (INTEGER_LIST, 0))) + +GAMMA: StaticSchema = ( + 18, + n0, + _G, + 0, + (_sN, _p), + ((7, {_sH: None}), (GAMMA_PAYLOAD, {_sP: None})), +) + +STREAM_SERVICE_ERROR: StaticSchema = (18, n0, _SSE, {_s: _se}, (_me,), ((3, 0),)) + +THROTTLING_EVENT: StaticSchema = (18, n0, _TE, {_s: _c}, (_me,), ((3, 0),)) + +TRADE_EVENTS: StaticSchema = ( + 19, + n0, + _TEr, + {_sm: None}, + (_a, _b, _g, _d, _th), + ((ALPHA, 0), (-1, 0), (GAMMA, 0), (DIFFERENT_SHAPE_NAME, 0), (THROTTLING_EVENT, 0)), +) + +TRADE_EVENT_STREAM_INPUT: StaticSchema = ( + 18, + n0, + _TESI, + {_smi: None}, + (_sI, _e), + ((3, 0), (TRADE_EVENTS, 0)), +) + +TRADE_EVENT_STREAM_OUTPUT: StaticSchema = ( + 18, + n0, + _TESO, + {_smit: None}, + (_gr, _e), + ((3, 0), (TRADE_EVENTS, 0)), +) + +TRADE_EVENT_STREAM: StaticOperationSchema = ( + 23, + n0, + _TES, + 0, + TRADE_EVENT_STREAM_INPUT, + TRADE_EVENT_STREAM_OUTPUT, + (STREAM_SERVICE_ERROR,), +) + +MY_HANDWRITTEN_JSON_MODEL: StaticSchema = ( + 21, + n0, + _MHJM, + {_sJ: MappingProxyType({"http": ("h2", "http/1.1"), "eventStreamHttp": ("h2",)})}, +) diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/__init__.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/__init__.py new file mode 100644 index 000000000..a7f592ce6 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/__init__.py @@ -0,0 +1,19 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from importlib import import_module +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .client import AsyncMyHandwrittenJsonModel + from .config import AsyncConfig + +__all__ = ["AsyncMyHandwrittenJsonModel", "AsyncConfig"] + +_EXPORTS = {"AsyncMyHandwrittenJsonModel": ".client", "AsyncConfig": ".config"} + + +def __getattr__(name: str) -> Any: + if name in _EXPORTS: + return getattr(import_module(_EXPORTS[name], __name__), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/client.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/client.py new file mode 100644 index 000000000..9fa60dbae --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/client.py @@ -0,0 +1,62 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +import logging + +from smithy_core.aio.client import AsyncClient +from smithy_core.aio.eventstream import AsyncDuplexEventStream +from smithy_http.plugins import user_agent_plugin + +from .. import models as _models +from ..models import ( + TradeEventStreamInput, + TradeEventStreamOutput, + TradeEvents, + _TradeEventsDeserializer, +) +from .config import AsyncConfig, Plugin +from .user_agent import aws_user_agent_plugin + + +logger = logging.getLogger(__name__) + + +class AsyncMyHandwrittenJsonModel(AsyncClient[AsyncConfig]): + """ + The rpcv2Json counterpart of + smithy.python.tests.handwritten#MyHandwrittenModel, used to drive the + duplex event-stream integration test over rpcv2Json. Shapes are local + copies (not shared with the CBOR service) so each service's shape + closure, default namespace, and generated client stay independent. + """ + + _client_plugins = (aws_user_agent_plugin, user_agent_plugin) + + async def trade_event_stream( + self, input: TradeEventStreamInput, plugins: list[Plugin] | None = None + ) -> AsyncDuplexEventStream[TradeEvents, TradeEvents, TradeEventStreamOutput]: + """ + Bidirectional (duplex) stream. The non-stream members (sessionId, + greeting) are carried as the initial-request and initial-response + messages. + + Args: + input: + An instance of `TradeEventStreamInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An `AsyncDuplexEventStream` for bidirectional streaming. + """ + pipeline, call = await self._prepare_call( + input, _models.TRADE_EVENT_STREAM, [], plugins + ) + return await pipeline.duplex_stream( + call, TradeEvents, TradeEvents, _TradeEventsDeserializer().deserialize + ) + + async def _new_config(self) -> AsyncConfig: + return AsyncConfig() diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/config.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/config.py new file mode 100644 index 000000000..f887dfe64 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/config.py @@ -0,0 +1,154 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass +from typing import Any, Callable, TYPE_CHECKING, TypeAlias, Union, cast + +from smithy_core.aio.interfaces import AsyncClientTransport, AsyncEndpointResolver +from smithy_core.aio.interfaces.auth import AsyncAuthScheme +from smithy_core.auth import DefaultAuthResolver +from smithy_core.endpoints import BddEndpointResolver +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, + URI, +) +from smithy_core.interfaces.auth import AuthSchemeResolver +from smithy_core.interfaces.retries import RetryStrategy +from smithy_core.retries import RetryStrategyOptions +from smithy_core.shapes import ShapeID +from smithy_http.aio.aiohttp import AIOHTTPClient +from smithy_http.aio.protocols import AsyncRpcV2JsonClientProtocol +from smithy_http.interfaces import HTTPRequestConfiguration + +from .._private.endpoints import BDD +from .._private.schemas import MY_HANDWRITTEN_JSON_MODEL + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import TradeEventStreamInput, TradeEventStreamOutput + + _ServiceInterceptor = Union[ + Interceptor[TradeEventStreamInput, TradeEventStreamOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="smithy.python.tests.handwritten.json", + service_target="MyHandwrittenJsonModel", + service_schema=MY_HANDWRITTEN_JSON_MODEL, +) + + +@dataclass(init=False) +class AsyncConfig: + """Configuration for MyHandwrittenJsonModel.""" + + auth_scheme_resolver: AuthSchemeResolver + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + auth_schemes: dict[ShapeID, AsyncAuthScheme[Any, Any, Any, Any]] + """A map of auth scheme ids to auth schemes.""" + + endpoint_resolver: EndpointResolver | AsyncEndpointResolver + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + endpoint_uri: str | URI | None + """A static URI to route requests to.""" + + http_request_config: HTTPRequestConfiguration | None + """Configuration for individual HTTP requests.""" + + interceptors: list[_ServiceInterceptor] + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + protocol: ClientProtocol[Any, Any] | None + """ + Pass a protocol class reference from smithy_aws_core.aio.protocols to + select the protocol, e.g. protocol=AsyncAwsJson10ClientProtocol. For + custom protocols a protocol instance may also be passed. + """ + + region: str | None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + retry_strategy: RetryStrategy | RetryStrategyOptions | None + """ + The retry strategy or options for configuring retry behavior. Can be + either a configured RetryStrategy or RetryStrategyOptions to create one. + """ + + sdk_ua_app_id: str | None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + transport: AsyncClientTransport[Any, Any] | None + """ + The transport to use to send requests (e.g. an HTTP client). Operations + with bidirectional event streams require a AsyncDuplexClientTransport, + such as AWSCRTHTTPClient. Transports are assumed not to support duplex + streaming unless they explicitly set SUPPORTS_DUPLEX_STREAMING to True. + """ + + user_agent_extra: str | None + """Additional suffix to be added to the User-Agent header.""" + + def __init__( + self, + *, + auth_scheme_resolver: AuthSchemeResolver | None = None, + auth_schemes: dict[ShapeID, AsyncAuthScheme[Any, Any, Any, Any]] | None = None, + endpoint_resolver: EndpointResolver | AsyncEndpointResolver | None = None, + endpoint_uri: str | URI | None = None, + http_request_config: HTTPRequestConfiguration | None = None, + interceptors: list[_ServiceInterceptor] | None = None, + protocol: ClientProtocol[Any, Any] + | ProtocolConstructor[ClientProtocol[Any, Any]] + | None = None, + region: str | None = None, + retry_strategy: RetryStrategy | RetryStrategyOptions | None = None, + sdk_ua_app_id: str | None = None, + transport: AsyncClientTransport[Any, Any] | None = None, + user_agent_extra: str | None = None, + ): + self.auth_scheme_resolver = auth_scheme_resolver or DefaultAuthResolver() + self.auth_schemes = auth_schemes or {} + self.endpoint_resolver = endpoint_resolver or BddEndpointResolver(BDD) + self.endpoint_uri = endpoint_uri + self.http_request_config = http_request_config + self.interceptors = interceptors or [] + if isinstance(protocol, type): + protocol = protocol(_PROTOCOL_SETTINGS) + self.protocol = cast( + "ClientProtocol[Any, Any]", protocol + ) or AsyncRpcV2JsonClientProtocol(_PROTOCOL_SETTINGS) + self.region = region + self.retry_strategy = retry_strategy + self.sdk_ua_app_id = sdk_ua_app_id + self.transport = transport or AIOHTTPClient() + self.user_agent_extra = user_agent_extra + + +Plugin: TypeAlias = Callable[[AsyncConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/user_agent.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/user_agent.py new file mode 100644 index 000000000..835581fb6 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/aio/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import AsyncConfig + + +def aws_user_agent_plugin(config: AsyncConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="MyHandwrittenJsonModel", + ) + ) diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/client.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/client.py new file mode 100644 index 000000000..c0a23c409 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/client.py @@ -0,0 +1,27 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +import logging + +from smithy_core.client import Client +from smithy_http.plugins import user_agent_plugin + +from .config import Config +from .user_agent import aws_user_agent_plugin + + +logger = logging.getLogger(__name__) + + +class MyHandwrittenJsonModel(Client[Config]): + """ + The rpcv2Json counterpart of + smithy.python.tests.handwritten#MyHandwrittenModel, used to drive the + duplex event-stream integration test over rpcv2Json. Shapes are local + copies (not shared with the CBOR service) so each service's shape + closure, default namespace, and generated client stay independent. + """ + + _client_plugins = (aws_user_agent_plugin, user_agent_plugin) + + def _new_config(self) -> Config: + return Config() diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/config.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/config.py new file mode 100644 index 000000000..1f62e06a4 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/config.py @@ -0,0 +1,153 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass +from typing import Any, Callable, TYPE_CHECKING, TypeAlias, Union, cast + +from smithy_core.auth import DefaultAuthResolver +from smithy_core.endpoints import BddEndpointResolver +from smithy_core.interfaces import ( + ClientProtocol, + ClientTransport, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, + URI, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.retries import RetryStrategy +from smithy_core.retries import RetryStrategyOptions +from smithy_core.shapes import ShapeID +from smithy_http.interfaces import HTTPRequestConfiguration +from smithy_http.protocols import RpcV2JsonClientProtocol +from smithy_http.urllib3 import Urllib3HTTPClient + +from ._private.endpoints import BDD +from ._private.schemas import MY_HANDWRITTEN_JSON_MODEL + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from .models import TradeEventStreamInput, TradeEventStreamOutput + + _ServiceInterceptor = Union[ + Interceptor[TradeEventStreamInput, TradeEventStreamOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="smithy.python.tests.handwritten.json", + service_target="MyHandwrittenJsonModel", + service_schema=MY_HANDWRITTEN_JSON_MODEL, +) + + +@dataclass(init=False) +class Config: + """Configuration for MyHandwrittenJsonModel.""" + + auth_scheme_resolver: AuthSchemeResolver + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] + """A map of auth scheme ids to auth schemes.""" + + endpoint_resolver: EndpointResolver + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + endpoint_uri: str | URI | None + """A static URI to route requests to.""" + + http_request_config: HTTPRequestConfiguration | None + """Configuration for individual HTTP requests.""" + + interceptors: list[_ServiceInterceptor] + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + protocol: ClientProtocol[Any, Any] | None + """ + Pass a protocol class reference from smithy_aws_core.aio.protocols to + select the protocol, e.g. protocol=AsyncAwsJson10ClientProtocol. For + custom protocols a protocol instance may also be passed. + """ + + region: str | None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + retry_strategy: RetryStrategy | RetryStrategyOptions | None + """ + The retry strategy or options for configuring retry behavior. Can be + either a configured RetryStrategy or RetryStrategyOptions to create one. + """ + + sdk_ua_app_id: str | None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + transport: ClientTransport[Any, Any] | None + """ + The transport to use to send requests (e.g. an HTTP client). Operations + with bidirectional event streams require a AsyncDuplexClientTransport, + such as AWSCRTHTTPClient. Transports are assumed not to support duplex + streaming unless they explicitly set SUPPORTS_DUPLEX_STREAMING to True. + """ + + user_agent_extra: str | None + """Additional suffix to be added to the User-Agent header.""" + + def __init__( + self, + *, + auth_scheme_resolver: AuthSchemeResolver | None = None, + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None, + endpoint_resolver: EndpointResolver | None = None, + endpoint_uri: str | URI | None = None, + http_request_config: HTTPRequestConfiguration | None = None, + interceptors: list[_ServiceInterceptor] | None = None, + protocol: ClientProtocol[Any, Any] + | ProtocolConstructor[ClientProtocol[Any, Any]] + | None = None, + region: str | None = None, + retry_strategy: RetryStrategy | RetryStrategyOptions | None = None, + sdk_ua_app_id: str | None = None, + transport: ClientTransport[Any, Any] | None = None, + user_agent_extra: str | None = None, + ): + self.auth_scheme_resolver = auth_scheme_resolver or DefaultAuthResolver() + self.auth_schemes = auth_schemes or {} + self.endpoint_resolver = endpoint_resolver or BddEndpointResolver(BDD) + self.endpoint_uri = endpoint_uri + self.http_request_config = http_request_config + self.interceptors = interceptors or [] + if isinstance(protocol, type): + protocol = protocol(_PROTOCOL_SETTINGS) + self.protocol = cast( + "ClientProtocol[Any, Any]", protocol + ) or RpcV2JsonClientProtocol(_PROTOCOL_SETTINGS) + self.region = region + self.retry_strategy = retry_strategy + self.sdk_ua_app_id = sdk_ua_app_id + self.transport = transport or Urllib3HTTPClient() + self.user_agent_extra = user_agent_extra + + +Plugin: TypeAlias = Callable[[Config], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/models.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/models.py new file mode 100644 index 000000000..34e81baca --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/models.py @@ -0,0 +1,85 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from ._private import schemas as _schemas +from smithy_core._dynamic_models import ModelRegistry +from smithy_core.exceptions import ModeledError +from smithy_core.schemas import APIOperation +from smithy_core.shapes import ShapeID + + +class ServiceError(ModeledError): + """ + Base error for all errors in the service. + + Some exceptions do not extend from this class, including + synthetic, implicit, and shared exception types. + """ + + +_registry = ModelRegistry( + _schemas, + renames={}, + index={ + "Alpha": "ALPHA", + "DifferentShapeName": "DIFFERENT_SHAPE_NAME", + "Gamma": "GAMMA", + "GammaPayload": "GAMMA_PAYLOAD", + "StreamServiceError": "STREAM_SERVICE_ERROR", + "ThrottlingEvent": "THROTTLING_EVENT", + "TradeEvents": "TRADE_EVENTS", + "TradeEventsAlpha": "TRADE_EVENTS", + "TradeEventsBeta": "TRADE_EVENTS", + "TradeEventsGamma": "TRADE_EVENTS", + "TradeEventsDelta": "TRADE_EVENTS", + "TradeEventsThrottle": "TRADE_EVENTS", + "TradeEventsUnknown": "TRADE_EVENTS", + "TradeEventStreamInput": "TRADE_EVENT_STREAM_INPUT", + "TradeEventStreamOutput": "TRADE_EVENT_STREAM_OUTPUT", + }, + shape_names=frozenset( + { + "Alpha", + "DifferentShapeName", + "Gamma", + "GammaPayload", + "StreamServiceError", + "ThrottlingEvent", + "TradeEventStreamInput", + "TradeEventStreamOutput", + "TradeEvents", + } + ), + service_error="ServiceError", +) + +_operation_data = {} +_operation_names = ("TRADE_EVENT_STREAM",) +for _operation_name in _operation_names: + globals().pop(_operation_name, None) + +__all__ = [name for name in globals() if not name.startswith("_")] +__all__.extend(_operation_names) + + +def __getattr__(name: str): + if (data := _operation_data.get(name)) is not None: + schema_name, error_ids, auth_scheme_ids = data + operation = APIOperation( + static_schema=getattr(_schemas, schema_name), + model_registry=_registry, + error_ids=[ShapeID(shape_id) for shape_id in error_ids], + effective_auth_schemes=[ShapeID(shape_id) for shape_id in auth_scheme_ids], + ) + return globals().setdefault(name, operation) + return _registry.build(name) + + +def __dir__(): + return sorted({*globals(), *_operation_names}) + + +_operation_data["TRADE_EVENT_STREAM"] = ( + "TRADE_EVENT_STREAM", + ("smithy.python.tests.handwritten.json#StreamServiceError",), + ("smithy.api#noAuth",), +) diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/models.pyi b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/models.pyi new file mode 100644 index 000000000..96b0929c4 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/models.pyi @@ -0,0 +1,150 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass +from datetime import datetime +from typing import Self, Union + +from smithy_core.deserializers import ShapeDeserializer +from smithy_core.exceptions import ModeledError +from smithy_core.response import ResponseMetadata +from smithy_core.schemas import APIOperation +from smithy_core.serializers import ShapeSerializer + +class _SerdeShape: + def serialize(self, serializer: ShapeSerializer) -> None: ... + def serialize_members(self, serializer: ShapeSerializer) -> None: ... + @classmethod + def deserialize(cls, deserializer: ShapeDeserializer) -> Self: ... + +class ServiceError(ModeledError): + """ + Base error for all errors in the service. + + Some exceptions do not extend from this class, including + synthetic, implicit, and shared exception types. + """ + +@dataclass(kw_only=True) +class Unit(_SerdeShape): + """Dataclass for Unit structure.""" + +@dataclass(kw_only=True) +class Alpha(_SerdeShape): + """Dataclass for Alpha structure.""" + + id: str | None = ... + timestamp: datetime | None = ... + +@dataclass(kw_only=True) +class DifferentShapeName(_SerdeShape): + """Dataclass for DifferentShapeName structure.""" + + name: str | None = ... + number: int | None = ... + +@dataclass(kw_only=True) +class Gamma(_SerdeShape): + """Dataclass for Gamma structure.""" + + sequence_number: int | None = ... + payload: GammaPayload | None = ... + +@dataclass(kw_only=True) +class GammaPayload(_SerdeShape): + """Dataclass for GammaPayload structure.""" + + message: str | None = ... + values: list[int] | None = ... + +@dataclass(kw_only=True) +class StreamServiceError(ServiceError, _SerdeShape): + """Dataclass for StreamServiceError structure.""" + + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class ThrottlingEvent(ServiceError, _SerdeShape): + """Dataclass for ThrottlingEvent structure.""" + + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass +class TradeEventsAlpha(_SerdeShape): + value: Alpha + +@dataclass +class TradeEventsBeta(_SerdeShape): + value: Unit + +@dataclass +class TradeEventsGamma(_SerdeShape): + value: Gamma + +@dataclass +class TradeEventsDelta(_SerdeShape): + value: DifferentShapeName + +@dataclass +class TradeEventsThrottle(_SerdeShape): + value: ThrottlingEvent + +@dataclass +class TradeEventsUnknown(_SerdeShape): + """ + Represents an unknown variant. + + If you receive this value, you will need to update your library to + receive the parsed value. + + This value may not be deliberately sent. + """ + + tag: str + +TradeEvents = Union[ + TradeEventsAlpha + | TradeEventsBeta + | TradeEventsGamma + | TradeEventsDelta + | TradeEventsThrottle + | TradeEventsUnknown +] + +class _TradeEventsDeserializer: + def deserialize(self, deserializer: ShapeDeserializer) -> TradeEvents: ... + +@dataclass(kw_only=True) +class TradeEventStreamInput(_SerdeShape): + """Dataclass for TradeEventStreamInput structure.""" + + session_id: str | None = ... + +@dataclass(kw_only=True) +class TradeEventStreamOutput(_SerdeShape): + """Dataclass for TradeEventStreamOutput structure.""" + + greeting: str | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +TRADE_EVENT_STREAM: APIOperation[TradeEventStreamInput, TradeEventStreamOutput] diff --git a/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/user_agent.py b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/user_agent.py new file mode 100644 index 000000000..97ccc68c4 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/src/my_handwritten_json_model/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from . import __version__ +from .config import Config + + +def aws_user_agent_plugin(config: Config): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="MyHandwrittenJsonModel", + ) + ) diff --git a/codegen-output/my-handwritten-json-model/tests/__init__.py b/codegen-output/my-handwritten-json-model/tests/__init__.py new file mode 100644 index 000000000..247be3e3d --- /dev/null +++ b/codegen-output/my-handwritten-json-model/tests/__init__.py @@ -0,0 +1 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. diff --git a/codegen-output/my-handwritten-json-model/tests/_typecheck.py b/codegen-output/my-handwritten-json-model/tests/_typecheck.py new file mode 100644 index 000000000..74e5d9f99 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/tests/_typecheck.py @@ -0,0 +1,27 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from typing import Any + +from my_handwritten_json_model.models import ( + Alpha, + DifferentShapeName, + Gamma, + GammaPayload, + TradeEventStreamInput, + TradeEventStreamOutput, + Unit, +) + + +# Static type fixture: pyright over this validates models.pyi. +# Not executed as a test; it only needs to type-check. + +_: Any = ... + +Unit() +Alpha() +DifferentShapeName() +Gamma() +GammaPayload() +TradeEventStreamInput() +TradeEventStreamOutput() diff --git a/codegen-output/my-handwritten-json-model/tests/test_model_conformance.py b/codegen-output/my-handwritten-json-model/tests/test_model_conformance.py new file mode 100644 index 000000000..63ae2aae5 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/tests/test_model_conformance.py @@ -0,0 +1,13 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from pathlib import Path + +from smithy_test import verify_models + +from my_handwritten_json_model import models as _models +from my_handwritten_json_model._private import schemas as _schemas + + +def test_model_conformance() -> None: + pyi = Path(_models.__file__).with_suffix(".pyi") + verify_models(_models, _schemas, pyi_path=str(pyi) if pyi.exists() else None) diff --git a/codegen-output/my-handwritten-json-model/tests/test_rpcv2json_protocol.py b/codegen-output/my-handwritten-json-model/tests/test_rpcv2json_protocol.py new file mode 100644 index 000000000..b19d7e088 --- /dev/null +++ b/codegen-output/my-handwritten-json-model/tests/test_rpcv2json_protocol.py @@ -0,0 +1,69 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_core.aio.types import AsyncBytesReader +from smithy_http import HTTPResponse as _smithy_http_HTTPResponse, tuples_to_fields +from smithy_http.interfaces import ( + HTTPClientConfiguration, + HTTPRequest, + HTTPRequestConfiguration, + HTTPResponse as _smithy_http_interfaces_HTTPResponse, +) + +from my_handwritten_json_model.models import ServiceError + + +class TestHttpServiceError(ServiceError): + """A test error that subclasses the service-error for protocol tests.""" + + def __init__(self, request: HTTPRequest): + self.request = request + + +class RequestTestHTTPClient: + """An asynchronous HTTP client solely for testing purposes.""" + + TIMEOUT_EXCEPTIONS = () + SUPPORTS_DUPLEX_STREAMING: bool = True + + def __init__(self, *, client_config: HTTPClientConfiguration | None = None): + self._client_config = client_config + + async def send( + self, + request: HTTPRequest, + *, + request_config: HTTPRequestConfiguration | None = None, + ) -> _smithy_http_interfaces_HTTPResponse: + # Raise the exception with the request object to bypass actual request handling + raise TestHttpServiceError(request) + + +class ResponseTestHTTPClient: + """An asynchronous HTTP client solely for testing purposes.""" + + TIMEOUT_EXCEPTIONS = () + SUPPORTS_DUPLEX_STREAMING: bool = True + + def __init__( + self, + *, + client_config: HTTPClientConfiguration | None = None, + status: int = 200, + headers: list[tuple[str, str]] | None = None, + body: bytes = b"", + ): + self._client_config = client_config + self.status = status + self.fields = tuples_to_fields(headers or []) + self.body = body + + async def send( + self, + request: HTTPRequest, + *, + request_config: HTTPRequestConfiguration | None = None, + ) -> _smithy_http_HTTPResponse: + # Pre-construct the response from the request and return it + return _smithy_http_HTTPResponse( + status=self.status, fields=self.fields, body=AsyncBytesReader(self.body) + ) diff --git a/codegen-output/rpcv2-json/README.md b/codegen-output/rpcv2-json/README.md new file mode 100644 index 000000000..76e479278 --- /dev/null +++ b/codegen-output/rpcv2-json/README.md @@ -0,0 +1,5 @@ + + +## RpcV2 JSON Protocol Service Client + +rpcv2json client diff --git a/codegen-output/rpcv2-json/pyproject.toml b/codegen-output/rpcv2-json/pyproject.toml new file mode 100644 index 000000000..294023a45 --- /dev/null +++ b/codegen-output/rpcv2-json/pyproject.toml @@ -0,0 +1,70 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +[project] +name = "rpcv2json" +version = "0.0.1" +description = "rpcv2json client" +readme = "README.md" +requires-python = ">=3.12" +keywords = ["smithy", "rpcv2json"] +license = {text = "Apache-2.0"} +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15" +] + +dependencies = [ + "smithy_core~=0.10.0", + "smithy_http[json, aiohttp, urllib3]~=0.7.0" +] + +[project.optional-dependencies] +awscrt = [ + "smithy_http[awscrt]~=0.7.0" +] + +[dependency-groups] +test = [ + "pytest>=9.0.3,<10.0.0", + "pytest-asyncio>=1.3.0,<1.4.0", + "smithy_test~=0.2.0" +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pyright] +typeCheckingMode = "strict" +reportPrivateUsage = false +reportUnusedFunction = false +reportUnusedVariable = false +reportUnnecessaryComparison = false +reportUnusedClass = false +enableExperimentalFeatures = true +# models.py is the dynamic runtime; models.pyi carries the typed +# surface for consumers, so don't type-check the implementation body. +exclude = ["**/models.py"] + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +ignore = ["F841"] + +[tool.ruff.format] +skip-magic-trailing-comma = true + +[tool.pytest.ini_options] +python_classes = ["!Test"] +asyncio_mode = "auto" diff --git a/codegen-output/rpcv2-json/src/rpcv2json/__init__.py b/codegen-output/rpcv2-json/src/rpcv2json/__init__.py new file mode 100644 index 000000000..eb79432a4 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/__init__.py @@ -0,0 +1,29 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from importlib import import_module +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .client import RpcV2JsonProtocol + from .config import Config + from .aio.client import AsyncRpcV2JsonProtocol + from .aio.config import AsyncConfig + +__all__ = ["RpcV2JsonProtocol", "Config", "AsyncRpcV2JsonProtocol", "AsyncConfig"] + +_EXPORTS = { + "RpcV2JsonProtocol": ".client", + "Config": ".config", + "AsyncRpcV2JsonProtocol": ".aio.client", + "AsyncConfig": ".aio.config", +} + + +def __getattr__(name: str) -> Any: + if name in _EXPORTS: + return getattr(import_module(_EXPORTS[name], __name__), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__version__: str = "0.0.1" diff --git a/codegen-output/rpcv2-json/src/rpcv2json/_private/__init__.py b/codegen-output/rpcv2-json/src/rpcv2json/_private/__init__.py new file mode 100644 index 000000000..247be3e3d --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/_private/__init__.py @@ -0,0 +1 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. diff --git a/codegen-output/rpcv2-json/src/rpcv2json/_private/endpoints.py b/codegen-output/rpcv2-json/src/rpcv2json/_private/endpoints.py new file mode 100644 index 000000000..8d7ee6335 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/_private/endpoints.py @@ -0,0 +1,31 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +"""Endpoint BDD for smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol.""" + +from smithy_core.endpoints import EndpointBdd + +# fmt: off +# Tags: ref=0, attr=1, call=2, list=3, map=4, error=5, endpoint=6. +BDD = EndpointBdd( + # (name, type, required, default, builtin, client_context_attribute) + parameters=( + ("endpoint", "string", False, None, "SDK::Endpoint", None), + ), + functions=("isSet",), + conditions=( + ((2, "isSet", (0, "endpoint")), None), # 0: isSet(endpoint) + ), + results=( + None, # 0: no match + (6, (0, "endpoint"), None, ()), # 1 + (5, "(default endpointRuleSet) endpoint is not set - you must configure an endpoint."), # 2 + ), + root=2, + # (condition, high, low); 1 and -1 are terminals, a negative reference is a + # complement edge, and 100_000_000 + k is result k. + nodes=( + -1, 1, -1, + 0, 100_000_001, 100_000_002, + ), +) +# fmt: on diff --git a/codegen-output/rpcv2-json/src/rpcv2json/_private/schemas.py b/codegen-output/rpcv2-json/src/rpcv2json/_private/schemas.py new file mode 100644 index 000000000..f86b58d04 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/_private/schemas.py @@ -0,0 +1,916 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from types import MappingProxyType + +from smithy_core._schema_compact import StaticOperationSchema, StaticSchema + + +n0 = "smithy.framework" +_VEF = "ValidationExceptionField" +_p = "path" +_s = "smithy.api#required" +_m = "message" +_VEFL = "ValidationExceptionFieldList" +_me = "member" +_VE = "ValidationException" +_sm = "smithy.api#error" +_c = "client" +_fL = "fieldList" +n1 = "smithy.protocoltests.rpcv2Json" +_BDOI = "BigDecimalOperationInput" +_sSI = "smithy.synthetic#originalShapeId" +_sJBDS = "smithy.protocoltests.rpcv2Json#BigDecimalStructure" +_smi = "smithy.api#input" +_v = "value" +_BDOO = "BigDecimalOperationOutput" +_smit = "smithy.api#output" +_BDO = "BigDecimalOperation" +_BIOI = "BigIntegerOperationInput" +_sJBIS = "smithy.protocoltests.rpcv2Json#BigIntegerStructure" +_BIOO = "BigIntegerOperationOutput" +_BIO = "BigIntegerOperation" +_COD = "ClientOptionalDefaults" +_smith = "smithy.api#default" +_CNED = "ComplexNestedErrorData" +_F = "Foo" +_CE = "ComplexError" +_TL = "TopLevel" +_N = "Nested" +_TE = "TestEnum" +_FOO = "FOO" +_sV = "smithy.api#enumValue" +_BAR = "BAR" +_BAZ = "BAZ" +_TIE = "TestIntEnum" +_ONE = "ONE" +_TWO = "TWO" +_TSL = "TestStringList" +_TSM = "TestStringMap" +_k = "key" +_D = "Defaults" +_dS = "defaultString" +_h = "hi" +_dB = "defaultBoolean" +_dL = "defaultList" +_dT = "defaultTimestamp" +_dBe = "defaultBlob" +_YWJ = "YWJj" +_dBef = "defaultByte" +_dSe = "defaultShort" +_dI = "defaultInteger" +_dLe = "defaultLong" +_dF = "defaultFloat" +_dD = "defaultDouble" +_dM = "defaultMap" +_dE = "defaultEnum" +_dIE = "defaultIntEnum" +_eS = "emptyString" +_v_ = "" +_fB = "falseBoolean" +_eB = "emptyBlob" +_zB = "zeroByte" +_zS = "zeroShort" +_zI = "zeroInteger" +_zL = "zeroLong" +_zF = "zeroFloat" +_zD = "zeroDouble" +_DBM = "DenseBooleanMap" +_DNM = "DenseNumberMap" +n2 = "smithy.protocoltests.shared" +_SS = "StringSet" +_DSM = "DenseSetMap" +_DSMe = "DenseStringMap" +_GS = "GreetingStruct" +_DSMen = "DenseStructMap" +_EIOI = "EmptyInputOutputInput" +_sJES = "smithy.protocoltests.rpcv2Json#EmptyStructure" +_EIOO = "EmptyInputOutputOutput" +_EIO = "EmptyInputOutput" +_FSI = "FractionalSecondsInput" +_sU = "smithy.api#Unit" +_FSO = "FractionalSecondsOutput" +_d = "datetime" +_FS = "FractionalSeconds" +_GWEI = "GreetingWithErrorsInput" +_GWEO = "GreetingWithErrorsOutput" +_g = "greeting" +_IG = "InvalidGreeting" +_M = "Message" +_GWE = "GreetingWithErrors" +_NIOI = "NoInputOutputInput" +_NIOO = "NoInputOutputOutput" +_NIO = "NoInputOutput" +_OWDI = "OperationWithDefaultsInput" +_de = "defaults" +_cOD = "clientOptionalDefaults" +_tLD = "topLevelDefault" +_oTLD = "otherTopLevelDefault" +_OWDO = "OperationWithDefaultsOutput" +_OWD = "OperationWithDefaults" +_OIOI = "OptionalInputOutputInput" +_sJSS = "smithy.protocoltests.rpcv2Json#SimpleStructure" +_OIOO = "OptionalInputOutputOutput" +_OIO = "OptionalInputOutput" +_RVJDMI = "RpcV2JsonDenseMapsInput" +_sJRVJDMIO = "smithy.protocoltests.rpcv2Json#RpcV2JsonDenseMapsInputOutput" +_dSM = "denseStructMap" +_dNM = "denseNumberMap" +_dBM = "denseBooleanMap" +_dSMe = "denseStringMap" +_dSMen = "denseSetMap" +_RVJDMO = "RpcV2JsonDenseMapsOutput" +_RVJDM = "RpcV2JsonDenseMaps" +_BL = "BlobList" +_BLo = "BooleanList" +_FE = "FooEnum" +_B = "Baz" +_Ba = "Bar" +_v__ = "1" +_ZERO = "ZERO" +_v___ = "0" +_FEL = "FooEnumList" +_IL = "IntegerList" +_IE = "IntegerEnum" +_A = "A" +_B_ = "B" +_C = "C" +_IEL = "IntegerEnumList" +_SL = "StringList" +_NSL = "NestedStringList" +_SLM = "StructureListMember" +_a = "a" +_b = "b" +_SLt = "StructureList" +_TLi = "TimestampList" +_RVJLI = "RpcV2JsonListsInput" +_sJRVJLIO = "smithy.protocoltests.rpcv2Json#RpcV2JsonListInputOutput" +_sL = "stringList" +_sS = "stringSet" +_iL = "integerList" +_bL = "booleanList" +_tL = "timestampList" +_eL = "enumList" +_iEL = "intEnumList" +_nSL = "nestedStringList" +_sLt = "structureList" +_bLl = "blobList" +_RVJLO = "RpcV2JsonListsOutput" +_RVJL = "RpcV2JsonLists" +_SBM = "SparseBooleanMap" +_smithy = "smithy.api#sparse" +_SNM = "SparseNumberMap" +_SSM = "SparseSetMap" +_SSMp = "SparseStringMap" +_SSMpa = "SparseStructMap" +_RVJSMI = "RpcV2JsonSparseMapsInput" +_sJRVJSMIO = "smithy.protocoltests.rpcv2Json#RpcV2JsonSparseMapsInputOutput" +_sSM = "sparseStructMap" +_sNM = "sparseNumberMap" +_sBM = "sparseBooleanMap" +_sSMp = "sparseStringMap" +_sSMpa = "sparseSetMap" +_RVJSMO = "RpcV2JsonSparseMapsOutput" +_RVJSM = "RpcV2JsonSparseMaps" +_SSPI = "SimpleScalarPropertiesInput" +_sJSSS = "smithy.protocoltests.rpcv2Json#SimpleScalarStructure" +_tBV = "trueBooleanValue" +_fBV = "falseBooleanValue" +_bV = "byteValue" +_dV = "doubleValue" +_fV = "floatValue" +_iV = "integerValue" +_lV = "longValue" +_sVh = "shortValue" +_sVt = "stringValue" +_bVl = "blobValue" +_SSPO = "SimpleScalarPropertiesOutput" +_SSP = "SimpleScalarProperties" +_SSL = "SparseStringList" +_SNOI = "SparseNullsOperationInput" +_sJSNOIO = "smithy.protocoltests.rpcv2Json#SparseNullsOperationInputOutput" +_sSL = "sparseStringList" +_SNOO = "SparseNullsOperationOutput" +_SNO = "SparseNullsOperation" +_DT = "DateTime" +_sF = "smithy.api#timestampFormat" +_dt = "date-time" +_ES = "EpochSeconds" +_es = "epoch-seconds" +_HD = "HttpDate" +_hd = "http-date" +_TFII = "TimestampFormatIgnoredInput" +_sJTFIIO = "smithy.protocoltests.rpcv2Json#TimestampFormatIgnoredIO" +_dTa = "dateTime" +_hD = "httpDate" +_eSp = "epochSeconds" +_n = "normal" +_TFIO = "TimestampFormatIgnoredOutput" +_TFI = "TimestampFormatIgnored" +_RSION = "RecursiveShapesInputOutputNested1" +_f = "foo" +_ne = "nested" +_RSIONe = "RecursiveShapesInputOutputNested2" +_ba = "bar" +_rM = "recursiveMember" +_RSI = "RecursiveShapesInput" +_sJRSIO = "smithy.protocoltests.rpcv2Json#RecursiveShapesInputOutput" +_RSO = "RecursiveShapesOutput" +_RS = "RecursiveShapes" +_RVJP = "RpcV2JsonProtocol" +_sJ = "smithy.protocols#rpcv2Json" + +VALIDATION_EXCEPTION_FIELD: StaticSchema = (18, n0, _VEF, 0, (_p, _m), ((3, 1), (3, 1))) + +VALIDATION_EXCEPTION_FIELD_LIST: StaticSchema = ( + 16, + n0, + _VEFL, + 0, + (_me,), + ((VALIDATION_EXCEPTION_FIELD, 0),), +) + +VALIDATION_EXCEPTION: StaticSchema = ( + 18, + n0, + _VE, + {_sm: _c}, + (_m, _fL), + ((3, 1), (VALIDATION_EXCEPTION_FIELD_LIST, 0)), +) + +BIG_DECIMAL_OPERATION_INPUT: StaticSchema = ( + 18, + n1, + _BDOI, + {_sSI: _sJBDS, _smi: None}, + (_v,), + ((12, 0),), +) + +BIG_DECIMAL_OPERATION_OUTPUT: StaticSchema = ( + 18, + n1, + _BDOO, + {_sSI: _sJBDS, _smit: None}, + (_v,), + ((12, 0),), +) + +BIG_DECIMAL_OPERATION: StaticOperationSchema = ( + 23, + n1, + _BDO, + 0, + BIG_DECIMAL_OPERATION_INPUT, + BIG_DECIMAL_OPERATION_OUTPUT, + (), +) + +BIG_INTEGER_OPERATION_INPUT: StaticSchema = ( + 18, + n1, + _BIOI, + {_sSI: _sJBIS, _smi: None}, + (_v,), + ((11, 0),), +) + +BIG_INTEGER_OPERATION_OUTPUT: StaticSchema = ( + 18, + n1, + _BIOO, + {_sSI: _sJBIS, _smit: None}, + (_v,), + ((11, 0),), +) + +BIG_INTEGER_OPERATION: StaticOperationSchema = ( + 23, + n1, + _BIO, + 0, + BIG_INTEGER_OPERATION_INPUT, + BIG_INTEGER_OPERATION_OUTPUT, + (), +) + +CLIENT_OPTIONAL_DEFAULTS: StaticSchema = (18, n1, _COD, 0, (_me,), ((7, {_smith: 0}),)) + +COMPLEX_NESTED_ERROR_DATA: StaticSchema = (18, n1, _CNED, 0, (_F,), ((3, 0),)) + +COMPLEX_ERROR: StaticSchema = ( + 18, + n1, + _CE, + {_sm: _c}, + (_TL, _N), + ((3, 0), (COMPLEX_NESTED_ERROR_DATA, 0)), +) + +TEST_ENUM: StaticSchema = ( + 14, + n1, + _TE, + 0, + (_FOO, _BAR, _BAZ), + ((-1, {_sV: _FOO}), (-1, {_sV: _BAR}), (-1, {_sV: _BAZ})), +) + +TEST_INT_ENUM: StaticSchema = ( + 15, + n1, + _TIE, + 0, + (_ONE, _TWO), + ((-1, {_sV: 1}), (-1, {_sV: 2})), +) + +TEST_STRING_LIST: StaticSchema = (16, n1, _TSL, 0, (_me,), ((3, 0),)) + +TEST_STRING_MAP: StaticSchema = (17, n1, _TSM, 0, (_k, _v), ((3, 0), (3, 0))) + +DEFAULTS: StaticSchema = ( + 18, + n1, + _D, + 0, + ( + _dS, + _dB, + _dL, + _dT, + _dBe, + _dBef, + _dSe, + _dI, + _dLe, + _dF, + _dD, + _dM, + _dE, + _dIE, + _eS, + _fB, + _eB, + _zB, + _zS, + _zI, + _zL, + _zF, + _zD, + ), + ( + (3, {_smith: _h}), + (2, {_smith: True}), + (TEST_STRING_LIST, {_smith: ()}), + (4, {_smith: 0}), + (1, {_smith: _YWJ}), + (5, {_smith: 1}), + (6, {_smith: 1}), + (7, {_smith: 10}), + (8, {_smith: 100}), + (9, {_smith: 1.0}), + (10, {_smith: 1.0}), + (TEST_STRING_MAP, {_smith: MappingProxyType[str, object]({})}), + (TEST_ENUM, {_smith: _FOO}), + (TEST_INT_ENUM, {_smith: 1}), + (3, {_smith: _v_}), + (2, {_smith: False}), + (1, {_smith: _v_}), + (5, {_smith: 0}), + (6, {_smith: 0}), + (7, {_smith: 0}), + (8, {_smith: 0}), + (9, {_smith: 0.0}), + (10, {_smith: 0.0}), + ), +) + +DENSE_BOOLEAN_MAP: StaticSchema = (17, n1, _DBM, 0, (_k, _v), ((3, 0), (2, 0))) + +DENSE_NUMBER_MAP: StaticSchema = (17, n1, _DNM, 0, (_k, _v), ((3, 0), (7, 0))) + +STRING_SET: StaticSchema = (16, n2, _SS, 0, (_me,), ((3, 0),)) + +DENSE_SET_MAP: StaticSchema = (17, n1, _DSM, 0, (_k, _v), ((3, 0), (STRING_SET, 0))) + +DENSE_STRING_MAP: StaticSchema = (17, n1, _DSMe, 0, (_k, _v), ((3, 0), (3, 0))) + +GREETING_STRUCT: StaticSchema = (18, n2, _GS, 0, (_h,), ((3, 0),)) + +DENSE_STRUCT_MAP: StaticSchema = ( + 17, + n1, + _DSMen, + 0, + (_k, _v), + ((3, 0), (GREETING_STRUCT, 0)), +) + +EMPTY_INPUT_OUTPUT_INPUT: StaticSchema = (18, n1, _EIOI, {_sSI: _sJES, _smi: None}) + +EMPTY_INPUT_OUTPUT_OUTPUT: StaticSchema = (18, n1, _EIOO, {_sSI: _sJES, _smit: None}) + +EMPTY_INPUT_OUTPUT: StaticOperationSchema = ( + 23, + n1, + _EIO, + 0, + EMPTY_INPUT_OUTPUT_INPUT, + EMPTY_INPUT_OUTPUT_OUTPUT, + (), +) + +FRACTIONAL_SECONDS_INPUT: StaticSchema = (18, n1, _FSI, {_sSI: _sU, _smi: None}) + +FRACTIONAL_SECONDS_OUTPUT: StaticSchema = ( + 18, + n1, + _FSO, + {_smit: None}, + (_d,), + ((4, 0),), +) + +FRACTIONAL_SECONDS: StaticOperationSchema = ( + 23, + n1, + _FS, + 0, + FRACTIONAL_SECONDS_INPUT, + FRACTIONAL_SECONDS_OUTPUT, + (), +) + +GREETING_WITH_ERRORS_INPUT: StaticSchema = (18, n1, _GWEI, {_sSI: _sU, _smi: None}) + +GREETING_WITH_ERRORS_OUTPUT: StaticSchema = ( + 18, + n1, + _GWEO, + {_smit: None}, + (_g,), + ((3, 0),), +) + +INVALID_GREETING: StaticSchema = (18, n1, _IG, {_sm: _c}, (_M,), ((3, 0),)) + +GREETING_WITH_ERRORS: StaticOperationSchema = ( + 23, + n1, + _GWE, + 0, + GREETING_WITH_ERRORS_INPUT, + GREETING_WITH_ERRORS_OUTPUT, + (INVALID_GREETING, COMPLEX_ERROR), +) + +NO_INPUT_OUTPUT_INPUT: StaticSchema = (18, n1, _NIOI, {_sSI: _sU, _smi: None}) + +NO_INPUT_OUTPUT_OUTPUT: StaticSchema = (18, n1, _NIOO, {_sSI: _sU, _smit: None}) + +NO_INPUT_OUTPUT: StaticOperationSchema = ( + 23, + n1, + _NIO, + 0, + NO_INPUT_OUTPUT_INPUT, + NO_INPUT_OUTPUT_OUTPUT, + (), +) + +OPERATION_WITH_DEFAULTS_INPUT: StaticSchema = ( + 18, + n1, + _OWDI, + {_smi: None}, + (_de, _cOD, _tLD, _oTLD), + ((DEFAULTS, 0), (CLIENT_OPTIONAL_DEFAULTS, 0), (3, {_smith: _h}), (7, {_smith: 0})), +) + +OPERATION_WITH_DEFAULTS_OUTPUT: StaticSchema = ( + 18, + n1, + _OWDO, + {_smit: None}, + ( + _dS, + _dB, + _dL, + _dT, + _dBe, + _dBef, + _dSe, + _dI, + _dLe, + _dF, + _dD, + _dM, + _dE, + _dIE, + _eS, + _fB, + _eB, + _zB, + _zS, + _zI, + _zL, + _zF, + _zD, + ), + ( + (3, {_smith: _h}), + (2, {_smith: True}), + (TEST_STRING_LIST, {_smith: ()}), + (4, {_smith: 0}), + (1, {_smith: _YWJ}), + (5, {_smith: 1}), + (6, {_smith: 1}), + (7, {_smith: 10}), + (8, {_smith: 100}), + (9, {_smith: 1.0}), + (10, {_smith: 1.0}), + (TEST_STRING_MAP, {_smith: MappingProxyType[str, object]({})}), + (TEST_ENUM, {_smith: _FOO}), + (TEST_INT_ENUM, {_smith: 1}), + (3, {_smith: _v_}), + (2, {_smith: False}), + (1, {_smith: _v_}), + (5, {_smith: 0}), + (6, {_smith: 0}), + (7, {_smith: 0}), + (8, {_smith: 0}), + (9, {_smith: 0.0}), + (10, {_smith: 0.0}), + ), +) + +OPERATION_WITH_DEFAULTS: StaticOperationSchema = ( + 23, + n1, + _OWD, + 0, + OPERATION_WITH_DEFAULTS_INPUT, + OPERATION_WITH_DEFAULTS_OUTPUT, + (VALIDATION_EXCEPTION,), +) + +OPTIONAL_INPUT_OUTPUT_INPUT: StaticSchema = ( + 18, + n1, + _OIOI, + {_sSI: _sJSS, _smi: None}, + (_v,), + ((3, 0),), +) + +OPTIONAL_INPUT_OUTPUT_OUTPUT: StaticSchema = ( + 18, + n1, + _OIOO, + {_sSI: _sJSS, _smit: None}, + (_v,), + ((3, 0),), +) + +OPTIONAL_INPUT_OUTPUT: StaticOperationSchema = ( + 23, + n1, + _OIO, + 0, + OPTIONAL_INPUT_OUTPUT_INPUT, + OPTIONAL_INPUT_OUTPUT_OUTPUT, + (), +) + +RPC_V2_JSON_DENSE_MAPS_INPUT: StaticSchema = ( + 18, + n1, + _RVJDMI, + {_sSI: _sJRVJDMIO, _smi: None}, + (_dSM, _dNM, _dBM, _dSMe, _dSMen), + ( + (DENSE_STRUCT_MAP, 0), + (DENSE_NUMBER_MAP, 0), + (DENSE_BOOLEAN_MAP, 0), + (DENSE_STRING_MAP, 0), + (DENSE_SET_MAP, 0), + ), +) + +RPC_V2_JSON_DENSE_MAPS_OUTPUT: StaticSchema = ( + 18, + n1, + _RVJDMO, + {_sSI: _sJRVJDMIO, _smit: None}, + (_dSM, _dNM, _dBM, _dSMe, _dSMen), + ( + (DENSE_STRUCT_MAP, 0), + (DENSE_NUMBER_MAP, 0), + (DENSE_BOOLEAN_MAP, 0), + (DENSE_STRING_MAP, 0), + (DENSE_SET_MAP, 0), + ), +) + +RPC_V2_JSON_DENSE_MAPS: StaticOperationSchema = ( + 23, + n1, + _RVJDM, + 0, + RPC_V2_JSON_DENSE_MAPS_INPUT, + RPC_V2_JSON_DENSE_MAPS_OUTPUT, + (VALIDATION_EXCEPTION,), +) + +BLOB_LIST: StaticSchema = (16, n2, _BL, 0, (_me,), ((1, 0),)) + +BOOLEAN_LIST: StaticSchema = (16, n2, _BLo, 0, (_me,), ((2, 0),)) + +FOO_ENUM: StaticSchema = ( + 14, + n2, + _FE, + 0, + (_FOO, _BAZ, _BAR, _ONE, _ZERO), + ( + (-1, {_sV: _F}), + (-1, {_sV: _B}), + (-1, {_sV: _Ba}), + (-1, {_sV: _v__}), + (-1, {_sV: _v___}), + ), +) + +FOO_ENUM_LIST: StaticSchema = (16, n2, _FEL, 0, (_me,), ((FOO_ENUM, 0),)) + +INTEGER_LIST: StaticSchema = (16, n2, _IL, 0, (_me,), ((7, 0),)) + +INTEGER_ENUM: StaticSchema = ( + 15, + n2, + _IE, + 0, + (_A, _B_, _C), + ((-1, {_sV: 1}), (-1, {_sV: 2}), (-1, {_sV: 3})), +) + +INTEGER_ENUM_LIST: StaticSchema = (16, n2, _IEL, 0, (_me,), ((INTEGER_ENUM, 0),)) + +STRING_LIST: StaticSchema = (16, n2, _SL, 0, (_me,), ((3, 0),)) + +NESTED_STRING_LIST: StaticSchema = (16, n2, _NSL, 0, (_me,), ((STRING_LIST, 0),)) + +STRUCTURE_LIST_MEMBER: StaticSchema = (18, n1, _SLM, 0, (_a, _b), ((3, 0), (3, 0))) + +STRUCTURE_LIST: StaticSchema = (16, n1, _SLt, 0, (_me,), ((STRUCTURE_LIST_MEMBER, 0),)) + +TIMESTAMP_LIST: StaticSchema = (16, n2, _TLi, 0, (_me,), ((4, 0),)) + +RPC_V2_JSON_LISTS_INPUT: StaticSchema = ( + 18, + n1, + _RVJLI, + {_sSI: _sJRVJLIO, _smi: None}, + (_sL, _sS, _iL, _bL, _tL, _eL, _iEL, _nSL, _sLt, _bLl), + ( + (STRING_LIST, 0), + (STRING_SET, 0), + (INTEGER_LIST, 0), + (BOOLEAN_LIST, 0), + (TIMESTAMP_LIST, 0), + (FOO_ENUM_LIST, 0), + (INTEGER_ENUM_LIST, 0), + (NESTED_STRING_LIST, 0), + (STRUCTURE_LIST, 0), + (BLOB_LIST, 0), + ), +) + +RPC_V2_JSON_LISTS_OUTPUT: StaticSchema = ( + 18, + n1, + _RVJLO, + {_sSI: _sJRVJLIO, _smit: None}, + (_sL, _sS, _iL, _bL, _tL, _eL, _iEL, _nSL, _sLt, _bLl), + ( + (STRING_LIST, 0), + (STRING_SET, 0), + (INTEGER_LIST, 0), + (BOOLEAN_LIST, 0), + (TIMESTAMP_LIST, 0), + (FOO_ENUM_LIST, 0), + (INTEGER_ENUM_LIST, 0), + (NESTED_STRING_LIST, 0), + (STRUCTURE_LIST, 0), + (BLOB_LIST, 0), + ), +) + +RPC_V2_JSON_LISTS: StaticOperationSchema = ( + 23, + n1, + _RVJL, + 0, + RPC_V2_JSON_LISTS_INPUT, + RPC_V2_JSON_LISTS_OUTPUT, + (VALIDATION_EXCEPTION,), +) + +SPARSE_BOOLEAN_MAP: StaticSchema = (17, n1, _SBM, 64, (_k, _v), ((3, 0), (2, 0))) + +SPARSE_NUMBER_MAP: StaticSchema = (17, n1, _SNM, 64, (_k, _v), ((3, 0), (7, 0))) + +SPARSE_SET_MAP: StaticSchema = (17, n1, _SSM, 64, (_k, _v), ((3, 0), (STRING_SET, 0))) + +SPARSE_STRING_MAP: StaticSchema = (17, n2, _SSMp, 64, (_k, _v), ((3, 0), (3, 0))) + +SPARSE_STRUCT_MAP: StaticSchema = ( + 17, + n1, + _SSMpa, + 64, + (_k, _v), + ((3, 0), (GREETING_STRUCT, 0)), +) + +RPC_V2_JSON_SPARSE_MAPS_INPUT: StaticSchema = ( + 18, + n1, + _RVJSMI, + {_sSI: _sJRVJSMIO, _smi: None}, + (_sSM, _sNM, _sBM, _sSMp, _sSMpa), + ( + (SPARSE_STRUCT_MAP, 0), + (SPARSE_NUMBER_MAP, 0), + (SPARSE_BOOLEAN_MAP, 0), + (SPARSE_STRING_MAP, 0), + (SPARSE_SET_MAP, 0), + ), +) + +RPC_V2_JSON_SPARSE_MAPS_OUTPUT: StaticSchema = ( + 18, + n1, + _RVJSMO, + {_sSI: _sJRVJSMIO, _smit: None}, + (_sSM, _sNM, _sBM, _sSMp, _sSMpa), + ( + (SPARSE_STRUCT_MAP, 0), + (SPARSE_NUMBER_MAP, 0), + (SPARSE_BOOLEAN_MAP, 0), + (SPARSE_STRING_MAP, 0), + (SPARSE_SET_MAP, 0), + ), +) + +RPC_V2_JSON_SPARSE_MAPS: StaticOperationSchema = ( + 23, + n1, + _RVJSM, + 0, + RPC_V2_JSON_SPARSE_MAPS_INPUT, + RPC_V2_JSON_SPARSE_MAPS_OUTPUT, + (VALIDATION_EXCEPTION,), +) + +SIMPLE_SCALAR_PROPERTIES_INPUT: StaticSchema = ( + 18, + n1, + _SSPI, + {_sSI: _sJSSS, _smi: None}, + (_tBV, _fBV, _bV, _dV, _fV, _iV, _lV, _sVh, _sVt, _bVl), + ((2, 0), (2, 0), (5, 0), (10, 0), (9, 0), (7, 0), (8, 0), (6, 0), (3, 0), (1, 0)), +) + +SIMPLE_SCALAR_PROPERTIES_OUTPUT: StaticSchema = ( + 18, + n1, + _SSPO, + {_sSI: _sJSSS, _smit: None}, + (_tBV, _fBV, _bV, _dV, _fV, _iV, _lV, _sVh, _sVt, _bVl), + ((2, 0), (2, 0), (5, 0), (10, 0), (9, 0), (7, 0), (8, 0), (6, 0), (3, 0), (1, 0)), +) + +SIMPLE_SCALAR_PROPERTIES: StaticOperationSchema = ( + 23, + n1, + _SSP, + 0, + SIMPLE_SCALAR_PROPERTIES_INPUT, + SIMPLE_SCALAR_PROPERTIES_OUTPUT, + (), +) + +SPARSE_STRING_LIST: StaticSchema = (16, n2, _SSL, 64, (_me,), ((3, 0),)) + +SPARSE_NULLS_OPERATION_INPUT: StaticSchema = ( + 18, + n1, + _SNOI, + {_sSI: _sJSNOIO, _smi: None}, + (_sSL, _sSMp), + ((SPARSE_STRING_LIST, 0), (SPARSE_STRING_MAP, 0)), +) + +SPARSE_NULLS_OPERATION_OUTPUT: StaticSchema = ( + 18, + n1, + _SNOO, + {_sSI: _sJSNOIO, _smit: None}, + (_sSL, _sSMp), + ((SPARSE_STRING_LIST, 0), (SPARSE_STRING_MAP, 0)), +) + +SPARSE_NULLS_OPERATION: StaticOperationSchema = ( + 23, + n1, + _SNO, + 0, + SPARSE_NULLS_OPERATION_INPUT, + SPARSE_NULLS_OPERATION_OUTPUT, + (), +) + +DATE_TIME: StaticSchema = (4, n2, _DT, {_sF: _dt}) + +EPOCH_SECONDS: StaticSchema = (4, n2, _ES, {_sF: _es}) + +HTTP_DATE: StaticSchema = (4, n2, _HD, {_sF: _hd}) + +TIMESTAMP_FORMAT_IGNORED_INPUT: StaticSchema = ( + 18, + n1, + _TFII, + {_sSI: _sJTFIIO, _smi: None}, + (_dTa, _hD, _eSp, _n), + ((DATE_TIME, 0), (HTTP_DATE, 0), (EPOCH_SECONDS, 0), (4, 0)), +) + +TIMESTAMP_FORMAT_IGNORED_OUTPUT: StaticSchema = ( + 18, + n1, + _TFIO, + {_sSI: _sJTFIIO, _smit: None}, + (_dTa, _hD, _eSp, _n), + ((DATE_TIME, 0), (HTTP_DATE, 0), (EPOCH_SECONDS, 0), (4, 0)), +) + +TIMESTAMP_FORMAT_IGNORED: StaticOperationSchema = ( + 23, + n1, + _TFI, + 0, + TIMESTAMP_FORMAT_IGNORED_INPUT, + TIMESTAMP_FORMAT_IGNORED_OUTPUT, + (), +) + +RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED1: StaticSchema = ( + 18, + n1, + _RSION, + 0, + (_f, _ne), + ((3, 0), (lambda: RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED2, 0)), +) + +RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED2: StaticSchema = ( + 18, + n1, + _RSIONe, + 0, + (_ba, _rM), + ((3, 0), (RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED1, 0)), +) + +RECURSIVE_SHAPES_INPUT: StaticSchema = ( + 18, + n1, + _RSI, + {_sSI: _sJRSIO, _smi: None}, + (_ne,), + ((RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED1, 0),), +) + +RECURSIVE_SHAPES_OUTPUT: StaticSchema = ( + 18, + n1, + _RSO, + {_sSI: _sJRSIO, _smit: None}, + (_ne,), + ((RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED1, 0),), +) + +RECURSIVE_SHAPES: StaticOperationSchema = ( + 23, + n1, + _RS, + 0, + RECURSIVE_SHAPES_INPUT, + RECURSIVE_SHAPES_OUTPUT, + (), +) + +RPC_V2_JSON_PROTOCOL: StaticSchema = (21, n1, _RVJP, {_sJ: None}) diff --git a/codegen-output/rpcv2-json/src/rpcv2json/aio/__init__.py b/codegen-output/rpcv2-json/src/rpcv2json/aio/__init__.py new file mode 100644 index 000000000..6e2ccfa4d --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/aio/__init__.py @@ -0,0 +1,19 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from importlib import import_module +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .client import AsyncRpcV2JsonProtocol + from .config import AsyncConfig + +__all__ = ["AsyncRpcV2JsonProtocol", "AsyncConfig"] + +_EXPORTS = {"AsyncRpcV2JsonProtocol": ".client", "AsyncConfig": ".config"} + + +def __getattr__(name: str) -> Any: + if name in _EXPORTS: + return getattr(import_module(_EXPORTS[name], __name__), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/codegen-output/rpcv2-json/src/rpcv2json/aio/client.py b/codegen-output/rpcv2-json/src/rpcv2json/aio/client.py new file mode 100644 index 000000000..958e2efa1 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/aio/client.py @@ -0,0 +1,377 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +import logging + +from smithy_core.aio.client import AsyncClient +from smithy_http.plugins import user_agent_plugin + +from .. import models as _models +from .config import AsyncConfig, Plugin +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + BigDecimalOperationInput, + BigDecimalOperationOutput, + BigIntegerOperationInput, + BigIntegerOperationOutput, + EmptyInputOutputInput, + EmptyInputOutputOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + NoInputOutputInput, + NoInputOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OptionalInputOutputInput, + OptionalInputOutputOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + RpcV2JsonDenseMapsInput, + RpcV2JsonDenseMapsOutput, + RpcV2JsonListsInput, + RpcV2JsonListsOutput, + RpcV2JsonSparseMapsInput, + RpcV2JsonSparseMapsOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + TimestampFormatIgnoredInput, + TimestampFormatIgnoredOutput, + ) + + +logger = logging.getLogger(__name__) + + +class AsyncRpcV2JsonProtocol(AsyncClient[AsyncConfig]): + """Client for RpcV2JsonProtocol""" + + _client_plugins = (aws_user_agent_plugin, user_agent_plugin) + + async def big_decimal_operation( + self, input: BigDecimalOperationInput, plugins: list[Plugin] | None = None + ) -> BigDecimalOperationOutput: + """ + Invokes the BigDecimalOperation operation. + + Args: + input: + An instance of `BigDecimalOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `BigDecimalOperationOutput`. + """ + return await self._invoke(input, _models.BIG_DECIMAL_OPERATION, [], plugins) + + async def big_integer_operation( + self, input: BigIntegerOperationInput, plugins: list[Plugin] | None = None + ) -> BigIntegerOperationOutput: + """ + Invokes the BigIntegerOperation operation. + + Args: + input: + An instance of `BigIntegerOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `BigIntegerOperationOutput`. + """ + return await self._invoke(input, _models.BIG_INTEGER_OPERATION, [], plugins) + + async def empty_input_output( + self, input: EmptyInputOutputInput, plugins: list[Plugin] | None = None + ) -> EmptyInputOutputOutput: + """ + Invokes the EmptyInputOutput operation. + + Args: + input: + An instance of `EmptyInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputOutputOutput`. + """ + return await self._invoke(input, _models.EMPTY_INPUT_OUTPUT, [], plugins) + + async def fractional_seconds( + self, input: FractionalSecondsInput, plugins: list[Plugin] | None = None + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + return await self._invoke(input, _models.FRACTIONAL_SECONDS, [], plugins) + + async def greeting_with_errors( + self, input: GreetingWithErrorsInput, plugins: list[Plugin] | None = None + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: + + 1. A successful response in the form of GreetingWithErrorsOutput + 2. An InvalidGreeting error. + 3. A ComplexError error. + + Implementations must be able to successfully take a response and + properly deserialize successful and error responses. + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + return await self._invoke(input, _models.GREETING_WITH_ERRORS, [], plugins) + + async def no_input_output( + self, input: NoInputOutputInput, plugins: list[Plugin] | None = None + ) -> NoInputOutputOutput: + """ + Invokes the NoInputOutput operation. + + Args: + input: + An instance of `NoInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputOutputOutput`. + """ + return await self._invoke(input, _models.NO_INPUT_OUTPUT, [], plugins) + + async def operation_with_defaults( + self, input: OperationWithDefaultsInput, plugins: list[Plugin] | None = None + ) -> OperationWithDefaultsOutput: + """ + Invokes the OperationWithDefaults operation. + + Args: + input: + An instance of `OperationWithDefaultsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithDefaultsOutput`. + """ + return await self._invoke(input, _models.OPERATION_WITH_DEFAULTS, [], plugins) + + async def optional_input_output( + self, input: OptionalInputOutputInput, plugins: list[Plugin] | None = None + ) -> OptionalInputOutputOutput: + """ + Invokes the OptionalInputOutput operation. + + Args: + input: + An instance of `OptionalInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OptionalInputOutputOutput`. + """ + return await self._invoke(input, _models.OPTIONAL_INPUT_OUTPUT, [], plugins) + + async def recursive_shapes( + self, input: RecursiveShapesInput, plugins: list[Plugin] | None = None + ) -> RecursiveShapesOutput: + """ + Invokes the RecursiveShapes operation. + + Args: + input: + An instance of `RecursiveShapesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RecursiveShapesOutput`. + """ + return await self._invoke(input, _models.RECURSIVE_SHAPES, [], plugins) + + async def rpc_v2_json_dense_maps( + self, input: RpcV2JsonDenseMapsInput, plugins: list[Plugin] | None = None + ) -> RpcV2JsonDenseMapsOutput: + """ + The example tests basic map serialization. + + Args: + input: + An instance of `RpcV2JsonDenseMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RpcV2JsonDenseMapsOutput`. + """ + return await self._invoke(input, _models.RPC_V2_JSON_DENSE_MAPS, [], plugins) + + async def rpc_v2_json_lists( + self, input: RpcV2JsonListsInput, plugins: list[Plugin] | None = None + ) -> RpcV2JsonListsOutput: + """ + This test case serializes JSON lists for the following cases for both + input and output: + + 1. Normal lists. + 2. Normal sets. + 3. Lists of lists. + 4. Lists of structures. + + Args: + input: + An instance of `RpcV2JsonListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RpcV2JsonListsOutput`. + """ + return await self._invoke(input, _models.RPC_V2_JSON_LISTS, [], plugins) + + async def rpc_v2_json_sparse_maps( + self, input: RpcV2JsonSparseMapsInput, plugins: list[Plugin] | None = None + ) -> RpcV2JsonSparseMapsOutput: + """ + Invokes the RpcV2JsonSparseMaps operation. + + Args: + input: + An instance of `RpcV2JsonSparseMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RpcV2JsonSparseMapsOutput`. + """ + return await self._invoke(input, _models.RPC_V2_JSON_SPARSE_MAPS, [], plugins) + + async def simple_scalar_properties( + self, input: SimpleScalarPropertiesInput, plugins: list[Plugin] | None = None + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + return await self._invoke(input, _models.SIMPLE_SCALAR_PROPERTIES, [], plugins) + + async def sparse_nulls_operation( + self, input: SparseNullsOperationInput, plugins: list[Plugin] | None = None + ) -> SparseNullsOperationOutput: + """ + Invokes the SparseNullsOperation operation. + + Args: + input: + An instance of `SparseNullsOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SparseNullsOperationOutput`. + """ + return await self._invoke(input, _models.SPARSE_NULLS_OPERATION, [], plugins) + + async def timestamp_format_ignored( + self, input: TimestampFormatIgnoredInput, plugins: list[Plugin] | None = None + ) -> TimestampFormatIgnoredOutput: + """ + This operation tests that the rpcv2Json protocol always uses + epoch-seconds for timestamp serialization, regardless of the + `timestampFormat` trait. Members targeting `DateTime` (date-time), + `HttpDate` (http-date), and `EpochSeconds` (epoch-seconds) must all + serialize as JSON numbers. + + Args: + input: + An instance of `TimestampFormatIgnoredInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TimestampFormatIgnoredOutput`. + """ + return await self._invoke(input, _models.TIMESTAMP_FORMAT_IGNORED, [], plugins) + + async def _new_config(self) -> AsyncConfig: + return AsyncConfig() diff --git a/codegen-output/rpcv2-json/src/rpcv2json/aio/config.py b/codegen-output/rpcv2-json/src/rpcv2json/aio/config.py new file mode 100644 index 000000000..f78623ac9 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/aio/config.py @@ -0,0 +1,203 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass +from typing import Any, Callable, TYPE_CHECKING, TypeAlias, Union, cast + +from smithy_core.aio.interfaces import AsyncClientTransport, AsyncEndpointResolver +from smithy_core.aio.interfaces.auth import AsyncAuthScheme +from smithy_core.auth import DefaultAuthResolver +from smithy_core.endpoints import BddEndpointResolver +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, + URI, +) +from smithy_core.interfaces.auth import AuthSchemeResolver +from smithy_core.interfaces.retries import RetryStrategy +from smithy_core.retries import RetryStrategyOptions +from smithy_core.shapes import ShapeID +from smithy_http.aio.aiohttp import AIOHTTPClient +from smithy_http.aio.protocols import AsyncRpcV2JsonClientProtocol +from smithy_http.interfaces import HTTPRequestConfiguration + +from .._private.endpoints import BDD +from .._private.schemas import RPC_V2_JSON_PROTOCOL + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + BigDecimalOperationInput, + BigDecimalOperationOutput, + BigIntegerOperationInput, + BigIntegerOperationOutput, + EmptyInputOutputInput, + EmptyInputOutputOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + NoInputOutputInput, + NoInputOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OptionalInputOutputInput, + OptionalInputOutputOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + RpcV2JsonDenseMapsInput, + RpcV2JsonDenseMapsOutput, + RpcV2JsonListsInput, + RpcV2JsonListsOutput, + RpcV2JsonSparseMapsInput, + RpcV2JsonSparseMapsOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + TimestampFormatIgnoredInput, + TimestampFormatIgnoredOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[BigDecimalOperationInput, BigDecimalOperationOutput, Any, Any], + Interceptor[BigIntegerOperationInput, BigIntegerOperationOutput, Any, Any], + Interceptor[EmptyInputOutputInput, EmptyInputOutputOutput, Any, Any], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[NoInputOutputInput, NoInputOutputOutput, Any, Any], + Interceptor[OperationWithDefaultsInput, OperationWithDefaultsOutput, Any, Any], + Interceptor[OptionalInputOutputInput, OptionalInputOutputOutput, Any, Any], + Interceptor[RecursiveShapesInput, RecursiveShapesOutput, Any, Any], + Interceptor[RpcV2JsonDenseMapsInput, RpcV2JsonDenseMapsOutput, Any, Any], + Interceptor[RpcV2JsonListsInput, RpcV2JsonListsOutput, Any, Any], + Interceptor[RpcV2JsonSparseMapsInput, RpcV2JsonSparseMapsOutput, Any, Any], + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + Interceptor[SparseNullsOperationInput, SparseNullsOperationOutput, Any, Any], + Interceptor[ + TimestampFormatIgnoredInput, TimestampFormatIgnoredOutput, Any, Any + ], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="smithy.protocoltests.rpcv2Json", + service_target="RpcV2JsonProtocol", + service_schema=RPC_V2_JSON_PROTOCOL, +) + + +@dataclass(init=False) +class AsyncConfig: + """Configuration for RpcV2JsonProtocol.""" + + auth_scheme_resolver: AuthSchemeResolver + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + auth_schemes: dict[ShapeID, AsyncAuthScheme[Any, Any, Any, Any]] + """A map of auth scheme ids to auth schemes.""" + + endpoint_resolver: EndpointResolver | AsyncEndpointResolver + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + endpoint_uri: str | URI | None + """A static URI to route requests to.""" + + http_request_config: HTTPRequestConfiguration | None + """Configuration for individual HTTP requests.""" + + interceptors: list[_ServiceInterceptor] + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + protocol: ClientProtocol[Any, Any] | None + """ + Pass a protocol class reference from smithy_aws_core.aio.protocols to + select the protocol, e.g. protocol=AsyncAwsJson10ClientProtocol. For + custom protocols a protocol instance may also be passed. + """ + + region: str | None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + retry_strategy: RetryStrategy | RetryStrategyOptions | None + """ + The retry strategy or options for configuring retry behavior. Can be + either a configured RetryStrategy or RetryStrategyOptions to create one. + """ + + sdk_ua_app_id: str | None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + transport: AsyncClientTransport[Any, Any] | None + """ + The transport to use to send requests (e.g. an HTTP client). Operations + with bidirectional event streams require a AsyncDuplexClientTransport, + such as AWSCRTHTTPClient. Transports are assumed not to support duplex + streaming unless they explicitly set SUPPORTS_DUPLEX_STREAMING to True. + """ + + user_agent_extra: str | None + """Additional suffix to be added to the User-Agent header.""" + + def __init__( + self, + *, + auth_scheme_resolver: AuthSchemeResolver | None = None, + auth_schemes: dict[ShapeID, AsyncAuthScheme[Any, Any, Any, Any]] | None = None, + endpoint_resolver: EndpointResolver | AsyncEndpointResolver | None = None, + endpoint_uri: str | URI | None = None, + http_request_config: HTTPRequestConfiguration | None = None, + interceptors: list[_ServiceInterceptor] | None = None, + protocol: ClientProtocol[Any, Any] + | ProtocolConstructor[ClientProtocol[Any, Any]] + | None = None, + region: str | None = None, + retry_strategy: RetryStrategy | RetryStrategyOptions | None = None, + sdk_ua_app_id: str | None = None, + transport: AsyncClientTransport[Any, Any] | None = None, + user_agent_extra: str | None = None, + ): + self.auth_scheme_resolver = auth_scheme_resolver or DefaultAuthResolver() + self.auth_schemes = auth_schemes or {} + self.endpoint_resolver = endpoint_resolver or BddEndpointResolver(BDD) + self.endpoint_uri = endpoint_uri + self.http_request_config = http_request_config + self.interceptors = interceptors or [] + if isinstance(protocol, type): + protocol = protocol(_PROTOCOL_SETTINGS) + self.protocol = cast( + "ClientProtocol[Any, Any]", protocol + ) or AsyncRpcV2JsonClientProtocol(_PROTOCOL_SETTINGS) + self.region = region + self.retry_strategy = retry_strategy + self.sdk_ua_app_id = sdk_ua_app_id + self.transport = transport or AIOHTTPClient() + self.user_agent_extra = user_agent_extra + + +Plugin: TypeAlias = Callable[[AsyncConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/rpcv2-json/src/rpcv2json/aio/user_agent.py b/codegen-output/rpcv2-json/src/rpcv2json/aio/user_agent.py new file mode 100644 index 000000000..1976459ce --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/aio/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import AsyncConfig + + +def aws_user_agent_plugin(config: AsyncConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="RpcV2JsonProtocol", + ) + ) diff --git a/codegen-output/rpcv2-json/src/rpcv2json/client.py b/codegen-output/rpcv2-json/src/rpcv2json/client.py new file mode 100644 index 000000000..84a0a9be4 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/client.py @@ -0,0 +1,407 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +import logging + +from smithy_core.client import Client +from smithy_http.plugins import user_agent_plugin + +from . import models as _models +from .config import Config, Plugin +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .models import ( + BigDecimalOperationInput, + BigDecimalOperationOutput, + BigIntegerOperationInput, + BigIntegerOperationOutput, + EmptyInputOutputInput, + EmptyInputOutputOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + NoInputOutputInput, + NoInputOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OptionalInputOutputInput, + OptionalInputOutputOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + RpcV2JsonDenseMapsInput, + RpcV2JsonDenseMapsOutput, + RpcV2JsonListsInput, + RpcV2JsonListsOutput, + RpcV2JsonSparseMapsInput, + RpcV2JsonSparseMapsOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + TimestampFormatIgnoredInput, + TimestampFormatIgnoredOutput, + ) + + +logger = logging.getLogger(__name__) + + +class RpcV2JsonProtocol(Client[Config]): + """Client for RpcV2JsonProtocol""" + + _client_plugins = (aws_user_agent_plugin, user_agent_plugin) + + def big_decimal_operation( + self, + input: BigDecimalOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> BigDecimalOperationOutput: + """ + Invokes the BigDecimalOperation operation. + + Args: + input: + An instance of `BigDecimalOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `BigDecimalOperationOutput`. + """ + return self._invoke(input, _models.BIG_DECIMAL_OPERATION, [], plugins) + + def big_integer_operation( + self, + input: BigIntegerOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> BigIntegerOperationOutput: + """ + Invokes the BigIntegerOperation operation. + + Args: + input: + An instance of `BigIntegerOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `BigIntegerOperationOutput`. + """ + return self._invoke(input, _models.BIG_INTEGER_OPERATION, [], plugins) + + def empty_input_output( + self, + input: EmptyInputOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyInputOutputOutput: + """ + Invokes the EmptyInputOutput operation. + + Args: + input: + An instance of `EmptyInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputOutputOutput`. + """ + return self._invoke(input, _models.EMPTY_INPUT_OUTPUT, [], plugins) + + def fractional_seconds( + self, + input: FractionalSecondsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + return self._invoke(input, _models.FRACTIONAL_SECONDS, [], plugins) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: + + 1. A successful response in the form of GreetingWithErrorsOutput + 2. An InvalidGreeting error. + 3. A ComplexError error. + + Implementations must be able to successfully take a response and + properly deserialize successful and error responses. + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + return self._invoke(input, _models.GREETING_WITH_ERRORS, [], plugins) + + def no_input_output( + self, + input: NoInputOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputOutputOutput: + """ + Invokes the NoInputOutput operation. + + Args: + input: + An instance of `NoInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputOutputOutput`. + """ + return self._invoke(input, _models.NO_INPUT_OUTPUT, [], plugins) + + def operation_with_defaults( + self, + input: OperationWithDefaultsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OperationWithDefaultsOutput: + """ + Invokes the OperationWithDefaults operation. + + Args: + input: + An instance of `OperationWithDefaultsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithDefaultsOutput`. + """ + return self._invoke(input, _models.OPERATION_WITH_DEFAULTS, [], plugins) + + def optional_input_output( + self, + input: OptionalInputOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OptionalInputOutputOutput: + """ + Invokes the OptionalInputOutput operation. + + Args: + input: + An instance of `OptionalInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OptionalInputOutputOutput`. + """ + return self._invoke(input, _models.OPTIONAL_INPUT_OUTPUT, [], plugins) + + def recursive_shapes( + self, + input: RecursiveShapesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RecursiveShapesOutput: + """ + Invokes the RecursiveShapes operation. + + Args: + input: + An instance of `RecursiveShapesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RecursiveShapesOutput`. + """ + return self._invoke(input, _models.RECURSIVE_SHAPES, [], plugins) + + def rpc_v2_json_dense_maps( + self, + input: RpcV2JsonDenseMapsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RpcV2JsonDenseMapsOutput: + """ + The example tests basic map serialization. + + Args: + input: + An instance of `RpcV2JsonDenseMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RpcV2JsonDenseMapsOutput`. + """ + return self._invoke(input, _models.RPC_V2_JSON_DENSE_MAPS, [], plugins) + + def rpc_v2_json_lists( + self, + input: RpcV2JsonListsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RpcV2JsonListsOutput: + """ + This test case serializes JSON lists for the following cases for both + input and output: + + 1. Normal lists. + 2. Normal sets. + 3. Lists of lists. + 4. Lists of structures. + + Args: + input: + An instance of `RpcV2JsonListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RpcV2JsonListsOutput`. + """ + return self._invoke(input, _models.RPC_V2_JSON_LISTS, [], plugins) + + def rpc_v2_json_sparse_maps( + self, + input: RpcV2JsonSparseMapsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RpcV2JsonSparseMapsOutput: + """ + Invokes the RpcV2JsonSparseMaps operation. + + Args: + input: + An instance of `RpcV2JsonSparseMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RpcV2JsonSparseMapsOutput`. + """ + return self._invoke(input, _models.RPC_V2_JSON_SPARSE_MAPS, [], plugins) + + def simple_scalar_properties( + self, + input: SimpleScalarPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + return self._invoke(input, _models.SIMPLE_SCALAR_PROPERTIES, [], plugins) + + def sparse_nulls_operation( + self, + input: SparseNullsOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SparseNullsOperationOutput: + """ + Invokes the SparseNullsOperation operation. + + Args: + input: + An instance of `SparseNullsOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SparseNullsOperationOutput`. + """ + return self._invoke(input, _models.SPARSE_NULLS_OPERATION, [], plugins) + + def timestamp_format_ignored( + self, + input: TimestampFormatIgnoredInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TimestampFormatIgnoredOutput: + """ + This operation tests that the rpcv2Json protocol always uses + epoch-seconds for timestamp serialization, regardless of the + `timestampFormat` trait. Members targeting `DateTime` (date-time), + `HttpDate` (http-date), and `EpochSeconds` (epoch-seconds) must all + serialize as JSON numbers. + + Args: + input: + An instance of `TimestampFormatIgnoredInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TimestampFormatIgnoredOutput`. + """ + return self._invoke(input, _models.TIMESTAMP_FORMAT_IGNORED, [], plugins) + + def _new_config(self) -> Config: + return Config() diff --git a/codegen-output/rpcv2-json/src/rpcv2json/config.py b/codegen-output/rpcv2-json/src/rpcv2json/config.py new file mode 100644 index 000000000..2698a288c --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/config.py @@ -0,0 +1,202 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass +from typing import Any, Callable, TYPE_CHECKING, TypeAlias, Union, cast + +from smithy_core.auth import DefaultAuthResolver +from smithy_core.endpoints import BddEndpointResolver +from smithy_core.interfaces import ( + ClientProtocol, + ClientTransport, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, + URI, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.retries import RetryStrategy +from smithy_core.retries import RetryStrategyOptions +from smithy_core.shapes import ShapeID +from smithy_http.interfaces import HTTPRequestConfiguration +from smithy_http.protocols import RpcV2JsonClientProtocol +from smithy_http.urllib3 import Urllib3HTTPClient + +from ._private.endpoints import BDD +from ._private.schemas import RPC_V2_JSON_PROTOCOL + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from .models import ( + BigDecimalOperationInput, + BigDecimalOperationOutput, + BigIntegerOperationInput, + BigIntegerOperationOutput, + EmptyInputOutputInput, + EmptyInputOutputOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + NoInputOutputInput, + NoInputOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OptionalInputOutputInput, + OptionalInputOutputOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + RpcV2JsonDenseMapsInput, + RpcV2JsonDenseMapsOutput, + RpcV2JsonListsInput, + RpcV2JsonListsOutput, + RpcV2JsonSparseMapsInput, + RpcV2JsonSparseMapsOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + TimestampFormatIgnoredInput, + TimestampFormatIgnoredOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[BigDecimalOperationInput, BigDecimalOperationOutput, Any, Any], + Interceptor[BigIntegerOperationInput, BigIntegerOperationOutput, Any, Any], + Interceptor[EmptyInputOutputInput, EmptyInputOutputOutput, Any, Any], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[NoInputOutputInput, NoInputOutputOutput, Any, Any], + Interceptor[OperationWithDefaultsInput, OperationWithDefaultsOutput, Any, Any], + Interceptor[OptionalInputOutputInput, OptionalInputOutputOutput, Any, Any], + Interceptor[RecursiveShapesInput, RecursiveShapesOutput, Any, Any], + Interceptor[RpcV2JsonDenseMapsInput, RpcV2JsonDenseMapsOutput, Any, Any], + Interceptor[RpcV2JsonListsInput, RpcV2JsonListsOutput, Any, Any], + Interceptor[RpcV2JsonSparseMapsInput, RpcV2JsonSparseMapsOutput, Any, Any], + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + Interceptor[SparseNullsOperationInput, SparseNullsOperationOutput, Any, Any], + Interceptor[ + TimestampFormatIgnoredInput, TimestampFormatIgnoredOutput, Any, Any + ], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="smithy.protocoltests.rpcv2Json", + service_target="RpcV2JsonProtocol", + service_schema=RPC_V2_JSON_PROTOCOL, +) + + +@dataclass(init=False) +class Config: + """Configuration for RpcV2JsonProtocol.""" + + auth_scheme_resolver: AuthSchemeResolver + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] + """A map of auth scheme ids to auth schemes.""" + + endpoint_resolver: EndpointResolver + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + endpoint_uri: str | URI | None + """A static URI to route requests to.""" + + http_request_config: HTTPRequestConfiguration | None + """Configuration for individual HTTP requests.""" + + interceptors: list[_ServiceInterceptor] + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + protocol: ClientProtocol[Any, Any] | None + """ + Pass a protocol class reference from smithy_aws_core.aio.protocols to + select the protocol, e.g. protocol=AsyncAwsJson10ClientProtocol. For + custom protocols a protocol instance may also be passed. + """ + + region: str | None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + retry_strategy: RetryStrategy | RetryStrategyOptions | None + """ + The retry strategy or options for configuring retry behavior. Can be + either a configured RetryStrategy or RetryStrategyOptions to create one. + """ + + sdk_ua_app_id: str | None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + transport: ClientTransport[Any, Any] | None + """ + The transport to use to send requests (e.g. an HTTP client). Operations + with bidirectional event streams require a AsyncDuplexClientTransport, + such as AWSCRTHTTPClient. Transports are assumed not to support duplex + streaming unless they explicitly set SUPPORTS_DUPLEX_STREAMING to True. + """ + + user_agent_extra: str | None + """Additional suffix to be added to the User-Agent header.""" + + def __init__( + self, + *, + auth_scheme_resolver: AuthSchemeResolver | None = None, + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None, + endpoint_resolver: EndpointResolver | None = None, + endpoint_uri: str | URI | None = None, + http_request_config: HTTPRequestConfiguration | None = None, + interceptors: list[_ServiceInterceptor] | None = None, + protocol: ClientProtocol[Any, Any] + | ProtocolConstructor[ClientProtocol[Any, Any]] + | None = None, + region: str | None = None, + retry_strategy: RetryStrategy | RetryStrategyOptions | None = None, + sdk_ua_app_id: str | None = None, + transport: ClientTransport[Any, Any] | None = None, + user_agent_extra: str | None = None, + ): + self.auth_scheme_resolver = auth_scheme_resolver or DefaultAuthResolver() + self.auth_schemes = auth_schemes or {} + self.endpoint_resolver = endpoint_resolver or BddEndpointResolver(BDD) + self.endpoint_uri = endpoint_uri + self.http_request_config = http_request_config + self.interceptors = interceptors or [] + if isinstance(protocol, type): + protocol = protocol(_PROTOCOL_SETTINGS) + self.protocol = cast( + "ClientProtocol[Any, Any]", protocol + ) or RpcV2JsonClientProtocol(_PROTOCOL_SETTINGS) + self.region = region + self.retry_strategy = retry_strategy + self.sdk_ua_app_id = sdk_ua_app_id + self.transport = transport or Urllib3HTTPClient() + self.user_agent_extra = user_agent_extra + + +Plugin: TypeAlias = Callable[[Config], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/rpcv2-json/src/rpcv2json/models.py b/codegen-output/rpcv2-json/src/rpcv2json/models.py new file mode 100644 index 000000000..688232221 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/models.py @@ -0,0 +1,246 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from ._private import schemas as _schemas +from smithy_core._dynamic_models import ModelRegistry +from smithy_core.exceptions import ModeledError +from smithy_core.schemas import APIOperation +from smithy_core.shapes import ShapeID + + +class ServiceError(ModeledError): + """ + Base error for all errors in the service. + + Some exceptions do not extend from this class, including + synthetic, implicit, and shared exception types. + """ + + +_registry = ModelRegistry( + _schemas, + renames={}, + index={ + "ValidationException": "VALIDATION_EXCEPTION", + "ValidationExceptionField": "VALIDATION_EXCEPTION_FIELD", + "BigDecimalOperationInput": "BIG_DECIMAL_OPERATION_INPUT", + "BigDecimalOperationOutput": "BIG_DECIMAL_OPERATION_OUTPUT", + "BigIntegerOperationInput": "BIG_INTEGER_OPERATION_INPUT", + "BigIntegerOperationOutput": "BIG_INTEGER_OPERATION_OUTPUT", + "ClientOptionalDefaults": "CLIENT_OPTIONAL_DEFAULTS", + "ComplexError": "COMPLEX_ERROR", + "ComplexNestedErrorData": "COMPLEX_NESTED_ERROR_DATA", + "Defaults": "DEFAULTS", + "EmptyInputOutputInput": "EMPTY_INPUT_OUTPUT_INPUT", + "EmptyInputOutputOutput": "EMPTY_INPUT_OUTPUT_OUTPUT", + "FractionalSecondsInput": "FRACTIONAL_SECONDS_INPUT", + "FractionalSecondsOutput": "FRACTIONAL_SECONDS_OUTPUT", + "GreetingWithErrorsInput": "GREETING_WITH_ERRORS_INPUT", + "GreetingWithErrorsOutput": "GREETING_WITH_ERRORS_OUTPUT", + "InvalidGreeting": "INVALID_GREETING", + "NoInputOutputInput": "NO_INPUT_OUTPUT_INPUT", + "NoInputOutputOutput": "NO_INPUT_OUTPUT_OUTPUT", + "OperationWithDefaultsInput": "OPERATION_WITH_DEFAULTS_INPUT", + "OperationWithDefaultsOutput": "OPERATION_WITH_DEFAULTS_OUTPUT", + "OptionalInputOutputInput": "OPTIONAL_INPUT_OUTPUT_INPUT", + "OptionalInputOutputOutput": "OPTIONAL_INPUT_OUTPUT_OUTPUT", + "RecursiveShapesInput": "RECURSIVE_SHAPES_INPUT", + "RecursiveShapesInputOutputNested1": "RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED1", + "RecursiveShapesInputOutputNested2": "RECURSIVE_SHAPES_INPUT_OUTPUT_NESTED2", + "RecursiveShapesOutput": "RECURSIVE_SHAPES_OUTPUT", + "RpcV2JsonDenseMapsInput": "RPC_V2_JSON_DENSE_MAPS_INPUT", + "RpcV2JsonDenseMapsOutput": "RPC_V2_JSON_DENSE_MAPS_OUTPUT", + "RpcV2JsonListsInput": "RPC_V2_JSON_LISTS_INPUT", + "RpcV2JsonListsOutput": "RPC_V2_JSON_LISTS_OUTPUT", + "RpcV2JsonSparseMapsInput": "RPC_V2_JSON_SPARSE_MAPS_INPUT", + "RpcV2JsonSparseMapsOutput": "RPC_V2_JSON_SPARSE_MAPS_OUTPUT", + "SimpleScalarPropertiesInput": "SIMPLE_SCALAR_PROPERTIES_INPUT", + "SimpleScalarPropertiesOutput": "SIMPLE_SCALAR_PROPERTIES_OUTPUT", + "SparseNullsOperationInput": "SPARSE_NULLS_OPERATION_INPUT", + "SparseNullsOperationOutput": "SPARSE_NULLS_OPERATION_OUTPUT", + "StructureListMember": "STRUCTURE_LIST_MEMBER", + "TestEnum": "TEST_ENUM", + "TestIntEnum": "TEST_INT_ENUM", + "TimestampFormatIgnoredInput": "TIMESTAMP_FORMAT_IGNORED_INPUT", + "TimestampFormatIgnoredOutput": "TIMESTAMP_FORMAT_IGNORED_OUTPUT", + "FooEnum": "FOO_ENUM", + "GreetingStruct": "GREETING_STRUCT", + "IntegerEnum": "INTEGER_ENUM", + }, + shape_names=frozenset( + { + "BigDecimalOperationInput", + "BigDecimalOperationOutput", + "BigIntegerOperationInput", + "BigIntegerOperationOutput", + "ClientOptionalDefaults", + "ComplexError", + "ComplexNestedErrorData", + "Defaults", + "EmptyInputOutputInput", + "EmptyInputOutputOutput", + "FooEnum", + "FractionalSecondsInput", + "FractionalSecondsOutput", + "GreetingStruct", + "GreetingWithErrorsInput", + "GreetingWithErrorsOutput", + "IntegerEnum", + "InvalidGreeting", + "NoInputOutputInput", + "NoInputOutputOutput", + "OperationWithDefaultsInput", + "OperationWithDefaultsOutput", + "OptionalInputOutputInput", + "OptionalInputOutputOutput", + "RecursiveShapesInput", + "RecursiveShapesInputOutputNested1", + "RecursiveShapesInputOutputNested2", + "RecursiveShapesOutput", + "RpcV2JsonDenseMapsInput", + "RpcV2JsonDenseMapsOutput", + "RpcV2JsonListsInput", + "RpcV2JsonListsOutput", + "RpcV2JsonSparseMapsInput", + "RpcV2JsonSparseMapsOutput", + "SimpleScalarPropertiesInput", + "SimpleScalarPropertiesOutput", + "SparseNullsOperationInput", + "SparseNullsOperationOutput", + "StructureListMember", + "TestEnum", + "TestIntEnum", + "TimestampFormatIgnoredInput", + "TimestampFormatIgnoredOutput", + "ValidationException", + "ValidationExceptionField", + } + ), + service_error="ServiceError", +) + +_operation_data = {} +_operation_names = ( + "BIG_DECIMAL_OPERATION", + "BIG_INTEGER_OPERATION", + "EMPTY_INPUT_OUTPUT", + "FRACTIONAL_SECONDS", + "GREETING_WITH_ERRORS", + "NO_INPUT_OUTPUT", + "OPERATION_WITH_DEFAULTS", + "OPTIONAL_INPUT_OUTPUT", + "RECURSIVE_SHAPES", + "RPC_V2_JSON_DENSE_MAPS", + "RPC_V2_JSON_LISTS", + "RPC_V2_JSON_SPARSE_MAPS", + "SIMPLE_SCALAR_PROPERTIES", + "SPARSE_NULLS_OPERATION", + "TIMESTAMP_FORMAT_IGNORED", +) +for _operation_name in _operation_names: + globals().pop(_operation_name, None) + +__all__ = [name for name in globals() if not name.startswith("_")] +__all__.extend(_operation_names) + + +def __getattr__(name: str): + if (data := _operation_data.get(name)) is not None: + schema_name, error_ids, auth_scheme_ids = data + operation = APIOperation( + static_schema=getattr(_schemas, schema_name), + model_registry=_registry, + error_ids=[ShapeID(shape_id) for shape_id in error_ids], + effective_auth_schemes=[ShapeID(shape_id) for shape_id in auth_scheme_ids], + ) + return globals().setdefault(name, operation) + return _registry.build(name) + + +def __dir__(): + return sorted({*globals(), *_operation_names}) + + +_operation_data["BIG_DECIMAL_OPERATION"] = ( + "BIG_DECIMAL_OPERATION", + (), + ("smithy.api#noAuth",), +) + +_operation_data["BIG_INTEGER_OPERATION"] = ( + "BIG_INTEGER_OPERATION", + (), + ("smithy.api#noAuth",), +) + +_operation_data["EMPTY_INPUT_OUTPUT"] = ( + "EMPTY_INPUT_OUTPUT", + (), + ("smithy.api#noAuth",), +) + +_operation_data["FRACTIONAL_SECONDS"] = ( + "FRACTIONAL_SECONDS", + (), + ("smithy.api#noAuth",), +) + +_operation_data["GREETING_WITH_ERRORS"] = ( + "GREETING_WITH_ERRORS", + ( + "smithy.protocoltests.rpcv2Json#InvalidGreeting", + "smithy.protocoltests.rpcv2Json#ComplexError", + ), + ("smithy.api#noAuth",), +) + +_operation_data["NO_INPUT_OUTPUT"] = ("NO_INPUT_OUTPUT", (), ("smithy.api#noAuth",)) + +_operation_data["OPERATION_WITH_DEFAULTS"] = ( + "OPERATION_WITH_DEFAULTS", + ("smithy.framework#ValidationException",), + ("smithy.api#noAuth",), +) + +_operation_data["OPTIONAL_INPUT_OUTPUT"] = ( + "OPTIONAL_INPUT_OUTPUT", + (), + ("smithy.api#noAuth",), +) + +_operation_data["RPC_V2_JSON_DENSE_MAPS"] = ( + "RPC_V2_JSON_DENSE_MAPS", + ("smithy.framework#ValidationException",), + ("smithy.api#noAuth",), +) + +_operation_data["RPC_V2_JSON_LISTS"] = ( + "RPC_V2_JSON_LISTS", + ("smithy.framework#ValidationException",), + ("smithy.api#noAuth",), +) + +_operation_data["RPC_V2_JSON_SPARSE_MAPS"] = ( + "RPC_V2_JSON_SPARSE_MAPS", + ("smithy.framework#ValidationException",), + ("smithy.api#noAuth",), +) + +_operation_data["SIMPLE_SCALAR_PROPERTIES"] = ( + "SIMPLE_SCALAR_PROPERTIES", + (), + ("smithy.api#noAuth",), +) + +_operation_data["SPARSE_NULLS_OPERATION"] = ( + "SPARSE_NULLS_OPERATION", + (), + ("smithy.api#noAuth",), +) + +_operation_data["TIMESTAMP_FORMAT_IGNORED"] = ( + "TIMESTAMP_FORMAT_IGNORED", + (), + ("smithy.api#noAuth",), +) + +_operation_data["RECURSIVE_SHAPES"] = ("RECURSIVE_SHAPES", (), ("smithy.api#noAuth",)) diff --git a/codegen-output/rpcv2-json/src/rpcv2json/models.pyi b/codegen-output/rpcv2-json/src/rpcv2json/models.pyi new file mode 100644 index 000000000..1b960222b --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/models.pyi @@ -0,0 +1,582 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal +from enum import IntEnum, StrEnum +from typing import Self + +from smithy_core.deserializers import ShapeDeserializer +from smithy_core.exceptions import ModeledError +from smithy_core.response import ResponseMetadata +from smithy_core.schemas import APIOperation +from smithy_core.serializers import ShapeSerializer +from smithy_core.types import UnknownEnumMixin + +class _SerdeShape: + def serialize(self, serializer: ShapeSerializer) -> None: ... + def serialize_members(self, serializer: ShapeSerializer) -> None: ... + @classmethod + def deserialize(cls, deserializer: ShapeDeserializer) -> Self: ... + +class ServiceError(ModeledError): + """ + Base error for all errors in the service. + + Some exceptions do not extend from this class, including + synthetic, implicit, and shared exception types. + """ + +@dataclass(kw_only=True) +class ValidationException(ServiceError, _SerdeShape): + """ + A standard error for input validation failures. This should be thrown by + services when a member of the input structure falls outside of the + modeled or documented constraints. + """ + + field_list: list[ValidationExceptionField] | None = ... + """ + A list of specific failures encountered while validating the input. A + member can appear in this list more than once if it failed to satisfy + multiple constraints. + """ + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class ValidationExceptionField(_SerdeShape): + """Describes one specific validation failure for an input member.""" + + path: str + """ + A JSONPointer expression to the structure member whose value failed to + satisfy the modeled constraints. + """ + message: str + """A detailed description of the validation failure.""" + +@dataclass(kw_only=True) +class BigDecimalOperationInput(_SerdeShape): + """Dataclass for BigDecimalOperationInput structure.""" + + value: Decimal | None = ... + +@dataclass(kw_only=True) +class BigDecimalOperationOutput(_SerdeShape): + """Dataclass for BigDecimalOperationOutput structure.""" + + value: Decimal | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class BigIntegerOperationInput(_SerdeShape): + """Dataclass for BigIntegerOperationInput structure.""" + + value: int | None = ... + +@dataclass(kw_only=True) +class BigIntegerOperationOutput(_SerdeShape): + """Dataclass for BigIntegerOperationOutput structure.""" + + value: int | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class ClientOptionalDefaults(_SerdeShape): + """Dataclass for ClientOptionalDefaults structure.""" + + member: int = ... + +@dataclass(kw_only=True) +class ComplexError(ServiceError, _SerdeShape): + """This error is thrown when a request is invalid.""" + + top_level: str | None = ... + nested: ComplexNestedErrorData | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class ComplexNestedErrorData(_SerdeShape): + """Dataclass for ComplexNestedErrorData structure.""" + + foo: str | None = ... + +@dataclass(kw_only=True) +class Defaults(_SerdeShape): + """Dataclass for Defaults structure.""" + + default_string: str = ... + default_boolean: bool = ... + default_list: list[str] = ... + default_timestamp: datetime = ... + default_blob: bytes = ... + default_byte: int = ... + default_short: int = ... + default_integer: int = ... + default_long: int = ... + default_float: float = ... + default_double: float = ... + default_map: dict[str, str] = ... + default_enum: str = ... + default_int_enum: int = ... + empty_string: str = ... + false_boolean: bool = ... + empty_blob: bytes = ... + zero_byte: int = ... + zero_short: int = ... + zero_integer: int = ... + zero_long: int = ... + zero_float: float = ... + zero_double: float = ... + +@dataclass(kw_only=True) +class EmptyInputOutputInput(_SerdeShape): + """Dataclass for EmptyInputOutputInput structure.""" + +@dataclass(kw_only=True) +class EmptyInputOutputOutput(_SerdeShape): + """Dataclass for EmptyInputOutputOutput structure.""" + + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class FractionalSecondsInput(_SerdeShape): + """Dataclass for FractionalSecondsInput structure.""" + +@dataclass(kw_only=True) +class FractionalSecondsOutput(_SerdeShape): + """Dataclass for FractionalSecondsOutput structure.""" + + datetime_: datetime | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class GreetingWithErrorsInput(_SerdeShape): + """Dataclass for GreetingWithErrorsInput structure.""" + +@dataclass(kw_only=True) +class GreetingWithErrorsOutput(_SerdeShape): + """Dataclass for GreetingWithErrorsOutput structure.""" + + greeting: str | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class InvalidGreeting(ServiceError, _SerdeShape): + """This error is thrown when an invalid greeting value is provided.""" + + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class NoInputOutputInput(_SerdeShape): + """Dataclass for NoInputOutputInput structure.""" + +@dataclass(kw_only=True) +class NoInputOutputOutput(_SerdeShape): + """Dataclass for NoInputOutputOutput structure.""" + + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class OperationWithDefaultsInput(_SerdeShape): + """Dataclass for OperationWithDefaultsInput structure.""" + + defaults: Defaults | None = ... + client_optional_defaults: ClientOptionalDefaults | None = ... + top_level_default: str = ... + other_top_level_default: int = ... + +@dataclass(kw_only=True) +class OperationWithDefaultsOutput(_SerdeShape): + """Dataclass for OperationWithDefaultsOutput structure.""" + + default_string: str = ... + default_boolean: bool = ... + default_list: list[str] = ... + default_timestamp: datetime = ... + default_blob: bytes = ... + default_byte: int = ... + default_short: int = ... + default_integer: int = ... + default_long: int = ... + default_float: float = ... + default_double: float = ... + default_map: dict[str, str] = ... + default_enum: str = ... + default_int_enum: int = ... + empty_string: str = ... + false_boolean: bool = ... + empty_blob: bytes = ... + zero_byte: int = ... + zero_short: int = ... + zero_integer: int = ... + zero_long: int = ... + zero_float: float = ... + zero_double: float = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class OptionalInputOutputInput(_SerdeShape): + """Dataclass for OptionalInputOutputInput structure.""" + + value: str | None = ... + +@dataclass(kw_only=True) +class OptionalInputOutputOutput(_SerdeShape): + """Dataclass for OptionalInputOutputOutput structure.""" + + value: str | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class RecursiveShapesInput(_SerdeShape): + """Dataclass for RecursiveShapesInput structure.""" + + nested: RecursiveShapesInputOutputNested1 | None = ... + +@dataclass(kw_only=True) +class RecursiveShapesInputOutputNested1(_SerdeShape): + """Dataclass for RecursiveShapesInputOutputNested1 structure.""" + + foo: str | None = ... + nested: RecursiveShapesInputOutputNested2 | None = ... + +@dataclass(kw_only=True) +class RecursiveShapesInputOutputNested2(_SerdeShape): + """Dataclass for RecursiveShapesInputOutputNested2 structure.""" + + bar: str | None = ... + recursive_member: RecursiveShapesInputOutputNested1 | None = ... + +@dataclass(kw_only=True) +class RecursiveShapesOutput(_SerdeShape): + """Dataclass for RecursiveShapesOutput structure.""" + + nested: RecursiveShapesInputOutputNested1 | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class RpcV2JsonDenseMapsInput(_SerdeShape): + """Dataclass for RpcV2JsonDenseMapsInput structure.""" + + dense_struct_map: dict[str, GreetingStruct] | None = ... + dense_number_map: dict[str, int] | None = ... + dense_boolean_map: dict[str, bool] | None = ... + dense_string_map: dict[str, str] | None = ... + dense_set_map: dict[str, list[str]] | None = ... + +@dataclass(kw_only=True) +class RpcV2JsonDenseMapsOutput(_SerdeShape): + """Dataclass for RpcV2JsonDenseMapsOutput structure.""" + + dense_struct_map: dict[str, GreetingStruct] | None = ... + dense_number_map: dict[str, int] | None = ... + dense_boolean_map: dict[str, bool] | None = ... + dense_string_map: dict[str, str] | None = ... + dense_set_map: dict[str, list[str]] | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class RpcV2JsonListsInput(_SerdeShape): + """Dataclass for RpcV2JsonListsInput structure.""" + + string_list: list[str] | None = ... + string_set: list[str] | None = ... + integer_list: list[int] | None = ... + boolean_list: list[bool] | None = ... + timestamp_list: list[datetime] | None = ... + enum_list: list[str] | None = ... + int_enum_list: list[int] | None = ... + nested_string_list: list[list[str]] | None = ... + """A list of lists of strings.""" + structure_list: list[StructureListMember] | None = ... + blob_list: list[bytes] | None = ... + +@dataclass(kw_only=True) +class RpcV2JsonListsOutput(_SerdeShape): + """Dataclass for RpcV2JsonListsOutput structure.""" + + string_list: list[str] | None = ... + string_set: list[str] | None = ... + integer_list: list[int] | None = ... + boolean_list: list[bool] | None = ... + timestamp_list: list[datetime] | None = ... + enum_list: list[str] | None = ... + int_enum_list: list[int] | None = ... + nested_string_list: list[list[str]] | None = ... + """A list of lists of strings.""" + structure_list: list[StructureListMember] | None = ... + blob_list: list[bytes] | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class RpcV2JsonSparseMapsInput(_SerdeShape): + """Dataclass for RpcV2JsonSparseMapsInput structure.""" + + sparse_struct_map: dict[str, GreetingStruct | None] | None = ... + sparse_number_map: dict[str, int | None] | None = ... + sparse_boolean_map: dict[str, bool | None] | None = ... + sparse_string_map: dict[str, str | None] | None = ... + sparse_set_map: dict[str, list[str] | None] | None = ... + +@dataclass(kw_only=True) +class RpcV2JsonSparseMapsOutput(_SerdeShape): + """Dataclass for RpcV2JsonSparseMapsOutput structure.""" + + sparse_struct_map: dict[str, GreetingStruct | None] | None = ... + sparse_number_map: dict[str, int | None] | None = ... + sparse_boolean_map: dict[str, bool | None] | None = ... + sparse_string_map: dict[str, str | None] | None = ... + sparse_set_map: dict[str, list[str] | None] | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class SimpleScalarPropertiesInput(_SerdeShape): + """Dataclass for SimpleScalarPropertiesInput structure.""" + + true_boolean_value: bool | None = ... + false_boolean_value: bool | None = ... + byte_value: int | None = ... + double_value: float | None = ... + float_value: float | None = ... + integer_value: int | None = ... + long_value: int | None = ... + short_value: int | None = ... + string_value: str | None = ... + blob_value: bytes | None = ... + +@dataclass(kw_only=True) +class SimpleScalarPropertiesOutput(_SerdeShape): + """Dataclass for SimpleScalarPropertiesOutput structure.""" + + true_boolean_value: bool | None = ... + false_boolean_value: bool | None = ... + byte_value: int | None = ... + double_value: float | None = ... + float_value: float | None = ... + integer_value: int | None = ... + long_value: int | None = ... + short_value: int | None = ... + string_value: str | None = ... + blob_value: bytes | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class SparseNullsOperationInput(_SerdeShape): + """Dataclass for SparseNullsOperationInput structure.""" + + sparse_string_list: list[str | None] | None = ... + sparse_string_map: dict[str, str | None] | None = ... + +@dataclass(kw_only=True) +class SparseNullsOperationOutput(_SerdeShape): + """Dataclass for SparseNullsOperationOutput structure.""" + + sparse_string_list: list[str | None] | None = ... + sparse_string_map: dict[str, str | None] | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +@dataclass(kw_only=True) +class StructureListMember(_SerdeShape): + """Dataclass for StructureListMember structure.""" + + a: str | None = ... + b: str | None = ... + +class TestEnum(UnknownEnumMixin, StrEnum): + FOO = "FOO" + BAR = "BAR" + BAZ = "BAZ" + +class TestIntEnum(UnknownEnumMixin, IntEnum): + ONE = 1 + TWO = 2 + +@dataclass(kw_only=True) +class TimestampFormatIgnoredInput(_SerdeShape): + """Dataclass for TimestampFormatIgnoredInput structure.""" + + date_time: datetime | None = ... + http_date: datetime | None = ... + epoch_seconds: datetime | None = ... + normal: datetime | None = ... + +@dataclass(kw_only=True) +class TimestampFormatIgnoredOutput(_SerdeShape): + """Dataclass for TimestampFormatIgnoredOutput structure.""" + + date_time: datetime | None = ... + http_date: datetime | None = ... + epoch_seconds: datetime | None = ... + normal: datetime | None = ... + response_metadata: ResponseMetadata = ... + """ + Metadata about the response that produced this output. + + Use this to recover the request identifiers a service's support team + needs in order to investigate a call. Members of the metadata are + individually optional. + """ + +class FooEnum(UnknownEnumMixin, StrEnum): + FOO = "Foo" + BAZ = "Baz" + BAR = "Bar" + ONE = "1" + ZERO = "0" + +@dataclass(kw_only=True) +class GreetingStruct(_SerdeShape): + """Dataclass for GreetingStruct structure.""" + + hi: str | None = ... + +class IntegerEnum(UnknownEnumMixin, IntEnum): + A = 1 + B = 2 + C = 3 + +BIG_DECIMAL_OPERATION: APIOperation[BigDecimalOperationInput, BigDecimalOperationOutput] +BIG_INTEGER_OPERATION: APIOperation[BigIntegerOperationInput, BigIntegerOperationOutput] +EMPTY_INPUT_OUTPUT: APIOperation[EmptyInputOutputInput, EmptyInputOutputOutput] +FRACTIONAL_SECONDS: APIOperation[FractionalSecondsInput, FractionalSecondsOutput] +GREETING_WITH_ERRORS: APIOperation[GreetingWithErrorsInput, GreetingWithErrorsOutput] +NO_INPUT_OUTPUT: APIOperation[NoInputOutputInput, NoInputOutputOutput] +OPERATION_WITH_DEFAULTS: APIOperation[ + OperationWithDefaultsInput, OperationWithDefaultsOutput +] +OPTIONAL_INPUT_OUTPUT: APIOperation[OptionalInputOutputInput, OptionalInputOutputOutput] +RECURSIVE_SHAPES: APIOperation[RecursiveShapesInput, RecursiveShapesOutput] +RPC_V2_JSON_DENSE_MAPS: APIOperation[RpcV2JsonDenseMapsInput, RpcV2JsonDenseMapsOutput] +RPC_V2_JSON_LISTS: APIOperation[RpcV2JsonListsInput, RpcV2JsonListsOutput] +RPC_V2_JSON_SPARSE_MAPS: APIOperation[ + RpcV2JsonSparseMapsInput, RpcV2JsonSparseMapsOutput +] +SIMPLE_SCALAR_PROPERTIES: APIOperation[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput +] +SPARSE_NULLS_OPERATION: APIOperation[ + SparseNullsOperationInput, SparseNullsOperationOutput +] +TIMESTAMP_FORMAT_IGNORED: APIOperation[ + TimestampFormatIgnoredInput, TimestampFormatIgnoredOutput +] diff --git a/codegen-output/rpcv2-json/src/rpcv2json/user_agent.py b/codegen-output/rpcv2-json/src/rpcv2json/user_agent.py new file mode 100644 index 000000000..a1cc90807 --- /dev/null +++ b/codegen-output/rpcv2-json/src/rpcv2json/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from . import __version__ +from .config import Config + + +def aws_user_agent_plugin(config: Config): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="RpcV2JsonProtocol", + ) + ) diff --git a/codegen-output/rpcv2-json/tests/__init__.py b/codegen-output/rpcv2-json/tests/__init__.py new file mode 100644 index 000000000..247be3e3d --- /dev/null +++ b/codegen-output/rpcv2-json/tests/__init__.py @@ -0,0 +1 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. diff --git a/codegen-output/rpcv2-json/tests/_typecheck.py b/codegen-output/rpcv2-json/tests/_typecheck.py new file mode 100644 index 000000000..8e515c128 --- /dev/null +++ b/codegen-output/rpcv2-json/tests/_typecheck.py @@ -0,0 +1,89 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from typing import Any + +from rpcv2json.models import ( + BigDecimalOperationInput, + BigDecimalOperationOutput, + BigIntegerOperationInput, + BigIntegerOperationOutput, + ClientOptionalDefaults, + ComplexNestedErrorData, + Defaults, + EmptyInputOutputInput, + EmptyInputOutputOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingStruct, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + NoInputOutputInput, + NoInputOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OptionalInputOutputInput, + OptionalInputOutputOutput, + RecursiveShapesInput, + RecursiveShapesInputOutputNested1, + RecursiveShapesInputOutputNested2, + RecursiveShapesOutput, + RpcV2JsonDenseMapsInput, + RpcV2JsonDenseMapsOutput, + RpcV2JsonListsInput, + RpcV2JsonListsOutput, + RpcV2JsonSparseMapsInput, + RpcV2JsonSparseMapsOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + StructureListMember, + TimestampFormatIgnoredInput, + TimestampFormatIgnoredOutput, + ValidationExceptionField, +) + + +# Static type fixture: pyright over this validates models.pyi. +# Not executed as a test; it only needs to type-check. + +_: Any = ... + +ValidationExceptionField(path=_, message=_) +BigDecimalOperationInput() +BigDecimalOperationOutput() +BigIntegerOperationInput() +BigIntegerOperationOutput() +ClientOptionalDefaults() +ComplexNestedErrorData() +Defaults() +EmptyInputOutputInput() +EmptyInputOutputOutput() +FractionalSecondsInput() +FractionalSecondsOutput() +GreetingWithErrorsInput() +GreetingWithErrorsOutput() +NoInputOutputInput() +NoInputOutputOutput() +OperationWithDefaultsInput() +OperationWithDefaultsOutput() +OptionalInputOutputInput() +OptionalInputOutputOutput() +RecursiveShapesInput() +RecursiveShapesInputOutputNested1() +RecursiveShapesInputOutputNested2() +RecursiveShapesOutput() +RpcV2JsonDenseMapsInput() +RpcV2JsonDenseMapsOutput() +RpcV2JsonListsInput() +RpcV2JsonListsOutput() +RpcV2JsonSparseMapsInput() +RpcV2JsonSparseMapsOutput() +SimpleScalarPropertiesInput() +SimpleScalarPropertiesOutput() +SparseNullsOperationInput() +SparseNullsOperationOutput() +StructureListMember() +TimestampFormatIgnoredInput() +TimestampFormatIgnoredOutput() +GreetingStruct() diff --git a/codegen-output/rpcv2-json/tests/test_model_conformance.py b/codegen-output/rpcv2-json/tests/test_model_conformance.py new file mode 100644 index 000000000..b570d2060 --- /dev/null +++ b/codegen-output/rpcv2-json/tests/test_model_conformance.py @@ -0,0 +1,13 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from pathlib import Path + +from smithy_test import verify_models + +from rpcv2json import models as _models +from rpcv2json._private import schemas as _schemas + + +def test_model_conformance() -> None: + pyi = Path(_models.__file__).with_suffix(".pyi") + verify_models(_models, _schemas, pyi_path=str(pyi) if pyi.exists() else None) diff --git a/codegen-output/rpcv2-json/tests/test_rpcv2json_protocol.py b/codegen-output/rpcv2-json/tests/test_rpcv2json_protocol.py new file mode 100644 index 000000000..e8d0ad2db --- /dev/null +++ b/codegen-output/rpcv2-json/tests/test_rpcv2json_protocol.py @@ -0,0 +1,4195 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from datetime import datetime, timezone +from decimal import Decimal +import json +from urllib.parse import parse_qsl + +from pytest import fail, mark, raises +from smithy_core.aio.types import AsyncBytesReader +from smithy_core.retries import SimpleRetryStrategy +from smithy_http import HTTPResponse as _smithy_http_HTTPResponse, tuples_to_fields +from smithy_http.interfaces import ( + HTTPClientConfiguration, + HTTPRequest, + HTTPRequestConfiguration, + HTTPResponse as _smithy_http_interfaces_HTTPResponse, +) +from smithy_test import deep_equal + +from rpcv2json.aio.client import AsyncRpcV2JsonProtocol +from rpcv2json.aio.config import AsyncConfig +from rpcv2json.models import ( + BigDecimalOperationInput, + BigDecimalOperationOutput, + BigIntegerOperationInput, + BigIntegerOperationOutput, + ClientOptionalDefaults, + Defaults, + EmptyInputOutputInput, + EmptyInputOutputOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingStruct, + GreetingWithErrorsInput, + NoInputOutputInput, + NoInputOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OptionalInputOutputInput, + OptionalInputOutputOutput, + RecursiveShapesInput, + RecursiveShapesInputOutputNested1, + RecursiveShapesInputOutputNested2, + RecursiveShapesOutput, + RpcV2JsonDenseMapsInput, + RpcV2JsonDenseMapsOutput, + RpcV2JsonListsInput, + RpcV2JsonListsOutput, + RpcV2JsonSparseMapsInput, + RpcV2JsonSparseMapsOutput, + ServiceError, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + StructureListMember, + TimestampFormatIgnoredInput, + TimestampFormatIgnoredOutput, +) + + +async def test_rpc_v2_json_request_big_decimal_simple_value_request_big_decimal_operation() -> ( + None +): + """ + Serializes a simple big decimal value as a JSON string to preserve + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigDecimalOperationInput(value=Decimal("1.5")) + + try: + await client.big_decimal_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigDecimalOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "1.5"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_big_decimal_high_precision_request_big_decimal_operation() -> ( + None +): + """ + Serializes a big decimal value that exceeds double precision in the + decimal portion. Implementations use JSON strings to preserve this + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigDecimalOperationInput(value=Decimal("0.100000000000000000000001")) + + try: + await client.big_decimal_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigDecimalOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "0.100000000000000000000001"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_big_decimal_negative_high_precision_request_big_decimal_operation() -> ( + None +): + """ + Serializes a negative big decimal value that exceeds double precision in + the decimal portion. Implementations use JSON strings to preserve this + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigDecimalOperationInput(value=Decimal("-0.100000000000000000000001")) + + try: + await client.big_decimal_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigDecimalOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "-0.100000000000000000000001"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_big_decimal_large_with_fraction_request_big_decimal_operation() -> ( + None +): + """ + Serializes a big decimal value that exceeds double precision in the + integer portion. Implementations use JSON strings to preserve this + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigDecimalOperationInput(value=Decimal("100000000000000000000001.0")) + + try: + await client.big_decimal_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigDecimalOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "100000000000000000000001.0"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_big_decimal_simple_value_response_big_decimal_operation() -> ( + None +): + """ + Serializes a simple big decimal value as a JSON string to preserve + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "1.5"\n}', + ), + ) + ) + + input_ = BigDecimalOperationInput() + + try: + actual = await client.big_decimal_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigDecimalOperationOutput(value=Decimal("1.5")) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_big_decimal_high_precision_response_big_decimal_operation() -> ( + None +): + """ + Serializes a big decimal value that exceeds double precision in the + decimal portion. Implementations use JSON strings to preserve this + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "0.100000000000000000000001"\n}', + ), + ) + ) + + input_ = BigDecimalOperationInput() + + try: + actual = await client.big_decimal_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigDecimalOperationOutput( + value=Decimal("0.100000000000000000000001") + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_big_decimal_negative_high_precision_response_big_decimal_operation() -> ( + None +): + """ + Serializes a negative big decimal value that exceeds double precision in + the decimal portion. Implementations use JSON strings to preserve this + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "-0.100000000000000000000001"\n}', + ), + ) + ) + + input_ = BigDecimalOperationInput() + + try: + actual = await client.big_decimal_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigDecimalOperationOutput( + value=Decimal("-0.100000000000000000000001") + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_big_decimal_large_with_fraction_response_big_decimal_operation() -> ( + None +): + """ + Serializes a big decimal value that exceeds double precision in the + integer portion. Implementations use JSON strings to preserve this + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "100000000000000000000001.0"\n}', + ), + ) + ) + + input_ = BigDecimalOperationInput() + + try: + actual = await client.big_decimal_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigDecimalOperationOutput( + value=Decimal("100000000000000000000001.0") + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_big_integer_simple_value_request_big_integer_operation() -> ( + None +): + """ + Serializes a simple big integer value as a JSON string to preserve + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigIntegerOperationInput(value=42) + + try: + await client.big_integer_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigIntegerOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "42"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_big_integer_exceeding_long_range_request_big_integer_operation() -> ( + None +): + """ + Serializes a big integer value that exceeds the precision of a long. + Implementations use JSON strings to preserve this precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigIntegerOperationInput(value=9223372036854775808) + + try: + await client.big_integer_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigIntegerOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "9223372036854775808"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_big_integer_negative_large_value_request_big_integer_operation() -> ( + None +): + """ + Serializes a negative big integer value that exceeds the precision of a + long. Implementations use JSON strings to preserve this precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = BigIntegerOperationInput(value=-9223372036854775809) + + try: + await client.big_integer_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/BigIntegerOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "value": "-9223372036854775809"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_big_integer_simple_value_response_big_integer_operation() -> ( + None +): + """ + Serializes a simple big integer value as a JSON string to preserve + precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "42"\n}', + ), + ) + ) + + input_ = BigIntegerOperationInput() + + try: + actual = await client.big_integer_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigIntegerOperationOutput(value=42) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_big_integer_exceeding_long_range_response_big_integer_operation() -> ( + None +): + """ + Serializes a big integer value that exceeds the precision of a long. + Implementations use JSON strings to preserve this precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "9223372036854775808"\n}', + ), + ) + ) + + input_ = BigIntegerOperationInput() + + try: + actual = await client.big_integer_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigIntegerOperationOutput(value=9223372036854775808) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_big_integer_negative_large_value_response_big_integer_operation() -> ( + None +): + """ + Serializes a negative big integer value that exceeds the precision of a + long. Implementations use JSON strings to preserve this precision. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "value": "-9223372036854775809"\n}', + ), + ) + ) + + input_ = BigIntegerOperationInput() + + try: + actual = await client.big_integer_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = BigIntegerOperationOutput(value=-9223372036854775809) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_empty_input_request_empty_input_output() -> None: + """When Input structure is empty we write an empty JSON object""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = EmptyInputOutputInput() + + try: + await client.empty_input_output(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/EmptyInputOutput" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set(["x-amz-target"]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b"{}" + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_empty_output_response_empty_input_output() -> None: + """When output structure is empty we write an empty JSON object""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b"{}", + ), + ) + ) + + input_ = EmptyInputOutputInput() + + try: + actual = await client.empty_input_output(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = EmptyInputOutputOutput() + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_empty_output_no_body_response_empty_input_output() -> ( + None +): + """When output structure is empty the client should accept an empty body""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b"", + ), + ) + ) + + input_ = EmptyInputOutputInput() + + try: + actual = await client.empty_input_output(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = EmptyInputOutputOutput() + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_date_time_with_fractional_seconds_response_fractional_seconds() -> ( + None +): + """ + Ensures that clients can correctly parse timestamps with fractional + seconds + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "datetime": 946845296.123\n}', + ), + ) + ) + + input_ = FractionalSecondsInput() + + try: + actual = await client.fractional_seconds(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = FractionalSecondsOutput( + datetime_=datetime(2000, 1, 2, 20, 34, 56, 123000, timezone.utc) + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_invalid_greeting_error_error_greeting_with_errors() -> ( + None +): + """Parses simple RpcV2 JSON errors""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=400, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "__type": "smithy.protocoltests.rpcv2Json#InvalidGreeting",\n "Message": "Hi"\n}', + ), + ) + ) + + input_ = GreetingWithErrorsInput() + + try: + await client.greeting_with_errors(input_) + fail("Expected 'InvalidGreeting' exception to be thrown!") + except Exception as err: + if type(err).__name__ != "InvalidGreeting": + fail( + f"Expected 'InvalidGreeting' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_complex_error_error_greeting_with_errors() -> None: + """Parses a complex error with no message member""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=400, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "__type": "smithy.protocoltests.rpcv2Json#ComplexError",\n "TopLevel": "Top level",\n "Nested": {\n "Foo": "bar"\n }\n}', + ), + ) + ) + + input_ = GreetingWithErrorsInput() + + try: + await client.greeting_with_errors(input_) + fail("Expected 'ComplexError' exception to be thrown!") + except Exception as err: + if type(err).__name__ != "ComplexError": + fail( + f"Expected 'ComplexError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_empty_complex_error_error_greeting_with_errors() -> ( + None +): + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=400, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "__type": "smithy.protocoltests.rpcv2Json#ComplexError"\n}', + ), + ) + ) + + input_ = GreetingWithErrorsInput() + + try: + await client.greeting_with_errors(input_) + fail("Expected 'ComplexError' exception to be thrown!") + except Exception as err: + if type(err).__name__ != "ComplexError": + fail( + f"Expected 'ComplexError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_no_input_request_no_input_output() -> None: + """Body is empty and no Content-Type header if no input""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = NoInputOutputInput() + + try: + await client.no_input_output(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/NoInputOutput" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set(["content-type", "x-amz-target"]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = [] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b"" + assert actual_body_content == expected_body_content + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_no_output_response_no_input_output() -> None: + """A `Content-Type` header should not be set if the response body is empty.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, headers=[("smithy-protocol", "rpc-v2-json")], body=b"" + ), + ) + ) + + input_ = NoInputOutputInput() + + try: + actual = await client.no_input_output(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = NoInputOutputOutput() + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_no_output_client_allows_empty_json_object_response_no_input_output() -> ( + None +): + """Clients should accept a JSON empty object if there is no output.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b"{}", + ), + ) + ) + + input_ = NoInputOutputInput() + + try: + actual = await client.no_input_output(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = NoInputOutputOutput() + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_no_output_client_allows_empty_body_response_no_input_output() -> ( + None +): + """ + Clients should accept an empty body if there is no output and should not + raise an error if the `Content-Type` header is set. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b"", + ), + ) + ) + + input_ = NoInputOutputInput() + + try: + actual = await client.no_input_output(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = NoInputOutputOutput() + + assert deep_equal(actual, expected) + + +@mark.xfail() +async def test_rpc_v2_json_request_client_populates_default_values_in_input_request_operation_with_defaults() -> ( + None +): + """Client populates default values in input.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = OperationWithDefaultsInput(defaults=Defaults()) + + try: + await client.operation_with_defaults(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/OperationWithDefaults" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "defaults": {\n "defaultString": "hi",\n "defaultBoolean": true,\n "defaultList": [],\n "defaultTimestamp": 0,\n "defaultBlob": "YWJj",\n "defaultByte": 1,\n "defaultShort": 1,\n "defaultInteger": 10,\n "defaultLong": 100,\n "defaultFloat": 1.0,\n "defaultDouble": 1.0,\n "defaultMap": {},\n "defaultEnum": "FOO",\n "defaultIntEnum": 1,\n "emptyString": "",\n "falseBoolean": false,\n "emptyBlob": "",\n "zeroByte": 0,\n "zeroShort": 0,\n "zeroInteger": 0,\n "zeroLong": 0,\n "zeroFloat": 0.0,\n "zeroDouble": 0.0\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +@mark.xfail() +async def test_rpc_v2_json_request_client_skips_top_level_default_values_in_input_request_operation_with_defaults() -> ( + None +): + """Client skips top level default values in input.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = OperationWithDefaultsInput() + + try: + await client.operation_with_defaults(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/OperationWithDefaults" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b"{}" + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +@mark.xfail() +async def test_rpc_v2_json_request_client_uses_explicitly_provided_member_values_over_defaults_request_operation_with_defaults() -> ( + None +): + """Client uses explicitly provided member values over defaults""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = OperationWithDefaultsInput( + defaults=Defaults( + default_string="bye", + default_boolean=True, + default_list=["a"], + default_timestamp=datetime(1970, 1, 1, 0, 0, 1, 0, timezone.utc), + default_blob=b"hi", + default_byte=2, + default_short=2, + default_integer=20, + default_long=200, + default_float=float(2.0), + default_double=float(2.0), + default_map={"name": "Jack"}, + default_enum="BAR", + default_int_enum=2, + empty_string="foo", + false_boolean=True, + empty_blob=b"hi", + zero_byte=1, + zero_short=1, + zero_integer=1, + zero_long=1, + zero_float=float(1.0), + zero_double=float(1.0), + ) + ) + + try: + await client.operation_with_defaults(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/OperationWithDefaults" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "defaults": {\n "defaultString": "bye",\n "defaultBoolean": true,\n "defaultList": ["a"],\n "defaultTimestamp": 1,\n "defaultBlob": "aGk=",\n "defaultByte": 2,\n "defaultShort": 2,\n "defaultInteger": 20,\n "defaultLong": 200,\n "defaultFloat": 2.0,\n "defaultDouble": 2.0,\n "defaultMap": {"name": "Jack"},\n "defaultEnum": "BAR",\n "defaultIntEnum": 2,\n "emptyString": "foo",\n "falseBoolean": true,\n "emptyBlob": "aGk=",\n "zeroByte": 1,\n "zeroShort": 1,\n "zeroInteger": 1,\n "zeroLong": 1,\n "zeroFloat": 1.0,\n "zeroDouble": 1.0\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_client_uses_explicitly_provided_values_in_top_level_request_operation_with_defaults() -> ( + None +): + """ + Any time a value is provided for a member in the top level of input, it + is used, regardless of if its the default. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = OperationWithDefaultsInput( + top_level_default="hi", other_top_level_default=0 + ) + + try: + await client.operation_with_defaults(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/OperationWithDefaults" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = ( + b'{\n "topLevelDefault": "hi",\n "otherTopLevelDefault": 0\n}' + ) + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +@mark.xfail() +async def test_rpc_v2_json_request_client_ignores_non_top_level_defaults_on_members_with_client_optional_request_operation_with_defaults() -> ( + None +): + """ + Typically, non top-level members would have defaults filled in, but if + they have the clientOptional trait, the defaults should be ignored. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = OperationWithDefaultsInput( + client_optional_defaults=ClientOptionalDefaults() + ) + + try: + await client.operation_with_defaults(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/OperationWithDefaults" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "clientOptionalDefaults": {}\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_client_populates_defaults_values_when_missing_in_response_response_operation_with_defaults() -> ( + None +): + """Client populates default values when missing in response.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b"{}", + ), + ) + ) + + input_ = OperationWithDefaultsInput() + + try: + actual = await client.operation_with_defaults(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = OperationWithDefaultsOutput( + default_string="hi", + default_boolean=True, + default_list=[], + default_timestamp=datetime(1970, 1, 1, 0, 0, 0, 0, timezone.utc), + default_blob=b"abc", + default_byte=1, + default_short=1, + default_integer=10, + default_long=100, + default_float=float(1.0), + default_double=float(1.0), + default_map={}, + default_enum="FOO", + default_int_enum=1, + empty_string="", + false_boolean=False, + empty_blob=b"", + zero_byte=0, + zero_short=0, + zero_integer=0, + zero_long=0, + zero_float=float(0.0), + zero_double=float(0.0), + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_client_ignores_default_values_if_member_values_are_present_in_response_response_operation_with_defaults() -> ( + None +): + """ + Client ignores default values if member values are present in the + response. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "defaultString": "bye",\n "defaultBoolean": false,\n "defaultList": ["a"],\n "defaultTimestamp": 2,\n "defaultBlob": "aGk=",\n "defaultByte": 2,\n "defaultShort": 2,\n "defaultInteger": 20,\n "defaultLong": 200,\n "defaultFloat": 2.0,\n "defaultDouble": 2.0,\n "defaultMap": {"name": "Jack"},\n "defaultEnum": "BAR",\n "defaultIntEnum": 2,\n "emptyString": "foo",\n "falseBoolean": true,\n "emptyBlob": "aGk=",\n "zeroByte": 1,\n "zeroShort": 1,\n "zeroInteger": 1,\n "zeroLong": 1,\n "zeroFloat": 1.0,\n "zeroDouble": 1.0\n}', + ), + ) + ) + + input_ = OperationWithDefaultsInput() + + try: + actual = await client.operation_with_defaults(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = OperationWithDefaultsOutput( + default_string="bye", + default_boolean=False, + default_list=["a"], + default_timestamp=datetime(1970, 1, 1, 0, 0, 2, 0, timezone.utc), + default_blob=b"hi", + default_byte=2, + default_short=2, + default_integer=20, + default_long=200, + default_float=float(2.0), + default_double=float(2.0), + default_map={"name": "Jack"}, + default_enum="BAR", + default_int_enum=2, + empty_string="foo", + false_boolean=True, + empty_blob=b"hi", + zero_byte=1, + zero_short=1, + zero_integer=1, + zero_long=1, + zero_float=float(1.0), + zero_double=float(1.0), + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_optional_input_request_optional_input_output() -> ( + None +): + """When input is empty we write an empty JSON object""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = OptionalInputOutputInput() + + try: + await client.optional_input_output(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/OptionalInputOutput" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set(["x-amz-target"]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = [] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b"{}" + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_optional_output_response_optional_input_output() -> ( + None +): + """When output is empty we write an empty JSON object""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b"{}", + ), + ) + ) + + input_ = OptionalInputOutputInput() + + try: + actual = await client.optional_input_output(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = OptionalInputOutputOutput() + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_recursive_shapes_request_recursive_shapes() -> None: + """Serializes recursive structures""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RecursiveShapesInput( + nested=RecursiveShapesInputOutputNested1( + foo="Foo1", + nested=RecursiveShapesInputOutputNested2( + bar="Bar1", + recursive_member=RecursiveShapesInputOutputNested1( + foo="Foo2", nested=RecursiveShapesInputOutputNested2(bar="Bar2") + ), + ), + ) + ) + + try: + await client.recursive_shapes(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RecursiveShapes" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "nested": {\n "foo": "Foo1",\n "nested": {\n "bar": "Bar1",\n "recursiveMember": {\n "foo": "Foo2",\n "nested": {\n "bar": "Bar2"\n }\n }\n }\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_recursive_shapes_response_recursive_shapes() -> ( + None +): + """Serializes recursive structures""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "nested": {\n "foo": "Foo1",\n "nested": {\n "bar": "Bar1",\n "recursiveMember": {\n "foo": "Foo2",\n "nested": {\n "bar": "Bar2"\n }\n }\n }\n }\n}', + ), + ) + ) + + input_ = RecursiveShapesInput() + + try: + actual = await client.recursive_shapes(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RecursiveShapesOutput( + nested=RecursiveShapesInputOutputNested1( + foo="Foo1", + nested=RecursiveShapesInputOutputNested2( + bar="Bar1", + recursive_member=RecursiveShapesInputOutputNested1( + foo="Foo2", nested=RecursiveShapesInputOutputNested2(bar="Bar2") + ), + ), + ) + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_maps_request_rpc_v2_json_dense_maps() -> None: + """Serializes maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonDenseMapsInput( + dense_struct_map={ + "foo": GreetingStruct(hi="there"), + "baz": GreetingStruct(hi="bye"), + } + ) + + try: + await client.rpc_v2_json_dense_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonDenseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "denseStructMap": {\n "foo": {\n "hi": "there"\n },\n "baz": {\n "hi": "bye"\n }\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_serializes_zero_values_in_maps_request_rpc_v2_json_dense_maps() -> ( + None +): + """Ensure that 0 and false are sent over the wire in all maps and lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonDenseMapsInput( + dense_number_map={"x": 0}, dense_boolean_map={"x": False} + ) + + try: + await client.rpc_v2_json_dense_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonDenseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "denseNumberMap": {\n "x": 0\n },\n "denseBooleanMap": {\n "x": false\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_serializes_dense_set_map_request_rpc_v2_json_dense_maps() -> ( + None +): + """A request that contains a dense map of sets.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonDenseMapsInput(dense_set_map={"x": [], "y": ["a", "b"]}) + + try: + await client.rpc_v2_json_dense_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonDenseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "denseSetMap": {\n "x": [],\n "y": ["a", "b"]\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_maps_response_rpc_v2_json_dense_maps() -> None: + """Deserializes maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "denseStructMap": {\n "foo": {\n "hi": "there"\n },\n "baz": {\n "hi": "bye"\n }\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonDenseMapsInput() + + try: + actual = await client.rpc_v2_json_dense_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonDenseMapsOutput( + dense_struct_map={ + "foo": GreetingStruct(hi="there"), + "baz": GreetingStruct(hi="bye"), + } + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_deserializes_zero_values_in_maps_response_rpc_v2_json_dense_maps() -> ( + None +): + """Ensure that 0 and false are sent over the wire in all maps and lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "denseNumberMap": {\n "x": 0\n },\n "denseBooleanMap": {\n "x": false\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonDenseMapsInput() + + try: + actual = await client.rpc_v2_json_dense_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonDenseMapsOutput( + dense_number_map={"x": 0}, dense_boolean_map={"x": False} + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_deserializes_dense_set_map_response_rpc_v2_json_dense_maps() -> ( + None +): + """A response that contains a dense map of sets""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "denseSetMap": {\n "x": [],\n "y": ["a", "b"]\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonDenseMapsInput() + + try: + actual = await client.rpc_v2_json_dense_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonDenseMapsOutput(dense_set_map={"x": [], "y": ["a", "b"]}) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_lists_request_rpc_v2_json_lists() -> None: + """Serializes RpcV2 JSON lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonListsInput( + string_list=["foo", "bar"], + string_set=["foo", "bar"], + integer_list=[1, 2], + boolean_list=[True, False], + timestamp_list=[ + datetime(2014, 4, 29, 18, 30, 38, 0, timezone.utc), + datetime(2014, 4, 29, 18, 30, 38, 0, timezone.utc), + ], + enum_list=["Foo", "0"], + int_enum_list=[1, 2], + nested_string_list=[["foo", "bar"], ["baz", "qux"]], + structure_list=[ + StructureListMember(a="1", b="2"), + StructureListMember(a="3", b="4"), + ], + blob_list=[b"foo", b"bar"], + ) + + try: + await client.rpc_v2_json_lists(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonLists" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "stringList": [\n "foo",\n "bar"\n ],\n "stringSet": [\n "foo",\n "bar"\n ],\n "integerList": [\n 1,\n 2\n ],\n "booleanList": [\n true,\n false\n ],\n "timestampList": [\n 1398796238,\n 1398796238\n ],\n "enumList": [\n "Foo",\n "0"\n ],\n "intEnumList": [\n 1,\n 2\n ],\n "nestedStringList": [\n [\n "foo",\n "bar"\n ],\n [\n "baz",\n "qux"\n ]\n ],\n "structureList": [\n {\n "a": "1",\n "b": "2"\n },\n {\n "a": "3",\n "b": "4"\n }\n ],\n "blobList": [\n "Zm9v",\n "YmFy"\n ]\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_lists_empty_request_rpc_v2_json_lists() -> None: + """Serializes empty JSON lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonListsInput(string_list=[]) + + try: + await client.rpc_v2_json_lists(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonLists" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "stringList": []\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_lists_response_rpc_v2_json_lists() -> None: + """Serializes RpcV2 JSON lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "stringList": [\n "foo",\n "bar"\n ],\n "stringSet": [\n "foo",\n "bar"\n ],\n "integerList": [\n 1,\n 2\n ],\n "booleanList": [\n true,\n false\n ],\n "timestampList": [\n 1398796238,\n 1398796238\n ],\n "enumList": [\n "Foo",\n "0"\n ],\n "intEnumList": [\n 1,\n 2\n ],\n "nestedStringList": [\n [\n "foo",\n "bar"\n ],\n [\n "baz",\n "qux"\n ]\n ],\n "structureList": [\n {\n "a": "1",\n "b": "2"\n },\n {\n "a": "3",\n "b": "4"\n }\n ],\n "blobList": [\n "Zm9v",\n "YmFy"\n ]\n}', + ), + ) + ) + + input_ = RpcV2JsonListsInput() + + try: + actual = await client.rpc_v2_json_lists(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonListsOutput( + string_list=["foo", "bar"], + string_set=["foo", "bar"], + integer_list=[1, 2], + boolean_list=[True, False], + timestamp_list=[ + datetime(2014, 4, 29, 18, 30, 38, 0, timezone.utc), + datetime(2014, 4, 29, 18, 30, 38, 0, timezone.utc), + ], + enum_list=["Foo", "0"], + int_enum_list=[1, 2], + nested_string_list=[["foo", "bar"], ["baz", "qux"]], + structure_list=[ + StructureListMember(a="1", b="2"), + StructureListMember(a="3", b="4"), + ], + blob_list=[b"foo", b"bar"], + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_lists_empty_response_rpc_v2_json_lists() -> None: + """Serializes empty RpcV2 JSON lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "stringList": []\n}', + ), + ) + ) + + input_ = RpcV2JsonListsInput() + + try: + actual = await client.rpc_v2_json_lists(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonListsOutput(string_list=[]) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_sparse_maps_request_rpc_v2_json_sparse_maps() -> ( + None +): + """Serializes sparse maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonSparseMapsInput( + sparse_struct_map={ + "foo": GreetingStruct(hi="there"), + "baz": GreetingStruct(hi="bye"), + } + ) + + try: + await client.rpc_v2_json_sparse_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonSparseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "sparseStructMap": {\n "foo": {\n "hi": "there"\n },\n "baz": {\n "hi": "bye"\n }\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_serializes_null_map_values_request_rpc_v2_json_sparse_maps() -> ( + None +): + """Serializes null map values in sparse maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonSparseMapsInput( + sparse_struct_map={"x": None}, + sparse_number_map={"x": None}, + sparse_boolean_map={"x": None}, + sparse_string_map={"x": None}, + ) + + try: + await client.rpc_v2_json_sparse_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonSparseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "sparseBooleanMap": {\n "x": null\n },\n "sparseNumberMap": {\n "x": null\n },\n "sparseStringMap": {\n "x": null\n },\n "sparseStructMap": {\n "x": null\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_serializes_sparse_set_map_request_rpc_v2_json_sparse_maps() -> ( + None +): + """A request that contains a sparse map of sets""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonSparseMapsInput(sparse_set_map={"x": [], "y": ["a", "b"]}) + + try: + await client.rpc_v2_json_sparse_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonSparseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "sparseSetMap": {\n "x": [],\n "y": ["a", "b"]\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_serializes_sparse_set_map_and_retains_null_request_rpc_v2_json_sparse_maps() -> ( + None +): + """A request that contains a sparse map of sets.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonSparseMapsInput( + sparse_set_map={"x": [], "y": ["a", "b"], "z": None} + ) + + try: + await client.rpc_v2_json_sparse_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonSparseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "sparseSetMap": {\n "x": [],\n "y": ["a", "b"],\n "z": null\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_serializes_zero_values_in_sparse_maps_request_rpc_v2_json_sparse_maps() -> ( + None +): + """Ensure that 0 and false are sent over the wire in all maps and lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = RpcV2JsonSparseMapsInput( + sparse_number_map={"x": 0}, sparse_boolean_map={"x": False} + ) + + try: + await client.rpc_v2_json_sparse_maps(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/RpcV2JsonSparseMaps" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "sparseNumberMap": {\n "x": 0\n },\n "sparseBooleanMap": {\n "x": false\n }\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_sparse_json_maps_response_rpc_v2_json_sparse_maps() -> ( + None +): + """Deserializes sparse maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseStructMap": {\n "foo": {\n "hi": "there"\n },\n "baz": {\n "hi": "bye"\n }\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonSparseMapsInput() + + try: + actual = await client.rpc_v2_json_sparse_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonSparseMapsOutput( + sparse_struct_map={ + "foo": GreetingStruct(hi="there"), + "baz": GreetingStruct(hi="bye"), + } + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_deserializes_null_map_values_response_rpc_v2_json_sparse_maps() -> ( + None +): + """Deserializes null map values""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseBooleanMap": {\n "x": null\n },\n "sparseNumberMap": {\n "x": null\n },\n "sparseStringMap": {\n "x": null\n },\n "sparseStructMap": {\n "x": null\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonSparseMapsInput() + + try: + actual = await client.rpc_v2_json_sparse_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonSparseMapsOutput( + sparse_struct_map={"x": None}, + sparse_number_map={"x": None}, + sparse_boolean_map={"x": None}, + sparse_string_map={"x": None}, + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_deserializes_sparse_set_map_response_rpc_v2_json_sparse_maps() -> ( + None +): + """A response that contains a sparse map of sets""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseSetMap": {\n "x": [],\n "y": ["a", "b"]\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonSparseMapsInput() + + try: + actual = await client.rpc_v2_json_sparse_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonSparseMapsOutput(sparse_set_map={"x": [], "y": ["a", "b"]}) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_deserializes_sparse_set_map_and_retains_null_response_rpc_v2_json_sparse_maps() -> ( + None +): + """A response that contains a sparse map of sets with a null""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseSetMap": {\n "x": [],\n "y": ["a", "b"],\n "z": null\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonSparseMapsInput() + + try: + actual = await client.rpc_v2_json_sparse_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonSparseMapsOutput( + sparse_set_map={"x": [], "y": ["a", "b"], "z": None} + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_deserializes_zero_values_in_sparse_maps_response_rpc_v2_json_sparse_maps() -> ( + None +): + """Ensure that 0 and false are sent over the wire in all maps and lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseNumberMap": {\n "x": 0\n },\n "sparseBooleanMap": {\n "x": false\n }\n}', + ), + ) + ) + + input_ = RpcV2JsonSparseMapsInput() + + try: + actual = await client.rpc_v2_json_sparse_maps(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = RpcV2JsonSparseMapsOutput( + sparse_number_map={"x": 0}, sparse_boolean_map={"x": False} + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_simple_scalar_properties_request_simple_scalar_properties() -> ( + None +): + """Serializes simple scalar properties""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SimpleScalarPropertiesInput( + true_boolean_value=True, + false_boolean_value=False, + byte_value=5, + double_value=float(1.889), + float_value=float(7.625), + integer_value=256, + long_value=9873, + short_value=9898, + string_value="simple", + blob_value=b"foo", + ) + + try: + await client.simple_scalar_properties(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SimpleScalarProperties" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "byteValue": 5,\n "doubleValue": 1.889,\n "falseBooleanValue": false,\n "floatValue": 7.625,\n "integerValue": 256,\n "longValue": 9873,\n "shortValue": 9898,\n "stringValue": "simple",\n "trueBooleanValue": true,\n "blobValue": "Zm9v"\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_client_doesnt_serialize_null_structure_values_request_simple_scalar_properties() -> ( + None +): + """RpcV2 JSON should not serialize null structure values""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SimpleScalarPropertiesInput(string_value=None) + + try: + await client.simple_scalar_properties(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SimpleScalarProperties" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b"{}" + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_supports_na_n_float_inputs_request_simple_scalar_properties() -> ( + None +): + """Supports handling NaN float values.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SimpleScalarPropertiesInput( + double_value=float("nan"), float_value=float("nan") + ) + + try: + await client.simple_scalar_properties(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SimpleScalarProperties" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = ( + b'{\n "doubleValue": "NaN",\n "floatValue": "NaN"\n}' + ) + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_supports_infinity_float_inputs_request_simple_scalar_properties() -> ( + None +): + """Supports handling Infinity float values.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SimpleScalarPropertiesInput( + double_value=float("inf"), float_value=float("inf") + ) + + try: + await client.simple_scalar_properties(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SimpleScalarProperties" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = ( + b'{\n "doubleValue": "Infinity",\n "floatValue": "Infinity"\n}' + ) + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_supports_negative_infinity_float_inputs_request_simple_scalar_properties() -> ( + None +): + """Supports handling -Infinity float values.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SimpleScalarPropertiesInput( + double_value=float("-inf"), float_value=float("-inf") + ) + + try: + await client.simple_scalar_properties(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SimpleScalarProperties" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = ( + b'{\n "doubleValue": "-Infinity",\n "floatValue": "-Infinity"\n}' + ) + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_simple_scalar_properties_response_simple_scalar_properties() -> ( + None +): + """Serializes simple scalar properties""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "trueBooleanValue": true,\n "falseBooleanValue": false,\n "byteValue": 5,\n "doubleValue": 1.889,\n "floatValue": 7.625,\n "integerValue": 256,\n "shortValue": 9898,\n "stringValue": "simple",\n "blobValue": "Zm9v"\n}', + ), + ) + ) + + input_ = SimpleScalarPropertiesInput() + + try: + actual = await client.simple_scalar_properties(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SimpleScalarPropertiesOutput( + true_boolean_value=True, + false_boolean_value=False, + byte_value=5, + double_value=float(1.889), + float_value=float(7.625), + integer_value=256, + short_value=9898, + string_value="simple", + blob_value=b"foo", + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_client_doesnt_deserialize_null_structure_values_response_simple_scalar_properties() -> ( + None +): + """RpcV2 JSON should not deserialize null structure values""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "stringValue": null\n}', + ), + ) + ) + + input_ = SimpleScalarPropertiesInput() + + try: + actual = await client.simple_scalar_properties(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SimpleScalarPropertiesOutput() + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_supports_na_n_float_outputs_response_simple_scalar_properties() -> ( + None +): + """Supports handling NaN float values.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "doubleValue": "NaN",\n "floatValue": "NaN"\n}', + ), + ) + ) + + input_ = SimpleScalarPropertiesInput() + + try: + actual = await client.simple_scalar_properties(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SimpleScalarPropertiesOutput( + double_value=float("nan"), float_value=float("nan") + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_supports_infinity_float_outputs_response_simple_scalar_properties() -> ( + None +): + """Supports handling Infinity float values.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "doubleValue": "Infinity",\n "floatValue": "Infinity"\n}', + ), + ) + ) + + input_ = SimpleScalarPropertiesInput() + + try: + actual = await client.simple_scalar_properties(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SimpleScalarPropertiesOutput( + double_value=float("inf"), float_value=float("inf") + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_supports_negative_infinity_float_outputs_response_simple_scalar_properties() -> ( + None +): + """Supports handling Negative Infinity float values.""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "doubleValue": "-Infinity",\n "floatValue": "-Infinity"\n}', + ), + ) + ) + + input_ = SimpleScalarPropertiesInput() + + try: + actual = await client.simple_scalar_properties(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SimpleScalarPropertiesOutput( + double_value=float("-inf"), float_value=float("-inf") + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_extra_fields_in_the_body_should_be_skipped_by_clients_response_simple_scalar_properties() -> ( + None +): + """ + The client should skip over additional fields that are not part of the + structure. This allows a client generated against an older Smithy model + to be able to communicate with a server that is generated against a + newer Smithy model. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "byteValue": 5,\n "doubleValue": 1.889,\n "falseBooleanValue": false,\n "floatValue": 7.625,\n "extraObject": {\n "normalString": "foo",\n "withAnArray": [1, 2, 3]\n },\n "integerValue": 256,\n "longValue": 9873,\n "shortValue": 9898,\n "stringValue": "simple",\n "someOtherField": "this should be skipped",\n "trueBooleanValue": true,\n "blobValue": "Zm9v"\n}', + ), + ) + ) + + input_ = SimpleScalarPropertiesInput() + + try: + actual = await client.simple_scalar_properties(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SimpleScalarPropertiesOutput( + true_boolean_value=True, + false_boolean_value=False, + byte_value=5, + double_value=float(1.889), + float_value=float(7.625), + integer_value=256, + long_value=9873, + short_value=9898, + string_value="simple", + blob_value=b"foo", + ) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_sparse_maps_serialize_null_values_request_sparse_nulls_operation() -> ( + None +): + """Serializes null values in maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SparseNullsOperationInput(sparse_string_map={"foo": None}) + + try: + await client.sparse_nulls_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SparseNullsOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = ( + b'{\n "sparseStringMap": {\n "foo": null\n }\n}' + ) + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_request_sparse_lists_serialize_null_request_sparse_nulls_operation() -> ( + None +): + """Serializes null values in lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = SparseNullsOperationInput(sparse_string_list=[None]) + + try: + await client.sparse_nulls_operation(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/SparseNullsOperation" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "sparseStringList": [\n null\n ]\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_sparse_maps_deserialize_null_values_response_sparse_nulls_operation() -> ( + None +): + """Deserializes null values in maps""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseStringMap": {\n "foo": null\n }\n}', + ), + ) + ) + + input_ = SparseNullsOperationInput() + + try: + actual = await client.sparse_nulls_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SparseNullsOperationOutput(sparse_string_map={"foo": None}) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_response_sparse_lists_deserialize_null_response_sparse_nulls_operation() -> ( + None +): + """Deserializes null values in lists""" + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "sparseStringList": [\n null\n ]\n}', + ), + ) + ) + + input_ = SparseNullsOperationInput() + + try: + actual = await client.sparse_nulls_operation(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = SparseNullsOperationOutput(sparse_string_list=[None]) + + assert deep_equal(actual, expected) + + +async def test_rpc_v2_json_request_timestamp_format_ignored_request_timestamp_format_ignored() -> ( + None +): + """ + The rpcv2Json protocol always serializes timestamps as epoch-seconds + JSON numbers. The timestampFormat trait MUST NOT be respected. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com/", + transport=RequestTestHTTPClient(), + retry_strategy=SimpleRetryStrategy(max_attempts=1), + ) + ) + + input_ = TimestampFormatIgnoredInput( + date_time=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + http_date=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + epoch_seconds=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + normal=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + ) + + try: + await client.timestamp_format_ignored(input_) + fail("Expected 'TestHttpServiceError' exception to be thrown!") + except TestHttpServiceError as err: + actual = err.request + + assert actual.method == "POST" + assert ( + actual.destination.path + == "/service/RpcV2JsonProtocol/operation/TimestampFormatIgnored" + ) + assert actual.destination.host == "example.com" + + query = actual.destination.query + actual_query_segments: list[str] = query.split("&") if query else [] + expected_query_segments: list[str] = [] + for expected_query_segment in expected_query_segments: + assert expected_query_segment in actual_query_segments + actual_query_segments.remove(expected_query_segment) + + actual_query_keys: list[str] = [k.lower() for k, v in parse_qsl(query)] + forbidden_query_keys: set[str] = set([]) + for forbidden_key in forbidden_query_keys: + assert forbidden_key.lower() not in actual_query_keys + + required_query_keys: list[str] = [] + for required_query_key in required_query_keys: + assert required_query_key.lower() in actual_query_keys + # These are removed because the required list could require more than one + # value. By removing each value after we assert that it's there, we can + # effectively validate that without having to have a more complex comparator. + actual_query_keys.remove(required_query_key) + + expected_headers: list[tuple[str, str]] = [ + ("accept", "application/json"), + ("content-type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ] + for expected_key, expected_val in expected_headers: + assert expected_val in actual.fields[expected_key].values + + forbidden_headers: set[str] = set([]) + for forbidden_key in forbidden_headers: + with raises(KeyError): + actual.fields[forbidden_key] + + required_headers: list[str] = ["content-length"] + for required_key in required_headers: + # del Fields[required_key] raises KeyError if key does not exist + del actual.fields[required_key] + + actual_body_content = await AsyncBytesReader(actual.body or b"").read() + expected_body_content = b'{\n "dateTime": 946845296,\n "httpDate": 946845296,\n "epochSeconds": 946845296,\n "normal": 946845296\n}' + actual_body = json.loads(actual_body_content) if actual_body_content else "" + expected_body = json.loads(expected_body_content) + assert actual_body == expected_body + + except Exception as err: + fail( + f"Expected 'TestHttpServiceError' exception to be thrown, but received {type(err).__name__}: {err}" + ) + + +async def test_rpc_v2_json_response_timestamp_format_ignored_response_timestamp_format_ignored() -> ( + None +): + """ + The rpcv2Json protocol always serializes timestamps as epoch-seconds + JSON numbers. The timestampFormat trait MUST NOT be respected. + """ + client = AsyncRpcV2JsonProtocol( + config=AsyncConfig( + endpoint_uri="https://example.com", + transport=ResponseTestHTTPClient( + status=200, + headers=[ + ("Content-Type", "application/json"), + ("smithy-protocol", "rpc-v2-json"), + ], + body=b'{\n "dateTime": 946845296,\n "httpDate": 946845296,\n "epochSeconds": 946845296,\n "normal": 946845296\n}', + ), + ) + ) + + input_ = TimestampFormatIgnoredInput() + + try: + actual = await client.timestamp_format_ignored(input_) + except Exception as err: + fail(f"Expected a valid response, but received: {type(err).__name__}: {err}") + else: + expected = TimestampFormatIgnoredOutput( + date_time=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + http_date=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + epoch_seconds=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + normal=datetime(2000, 1, 2, 20, 34, 56, 0, timezone.utc), + ) + + assert deep_equal(actual, expected) + + +class TestHttpServiceError(ServiceError): + """A test error that subclasses the service-error for protocol tests.""" + + def __init__(self, request: HTTPRequest): + self.request = request + + +class RequestTestHTTPClient: + """An asynchronous HTTP client solely for testing purposes.""" + + TIMEOUT_EXCEPTIONS = () + SUPPORTS_DUPLEX_STREAMING: bool = True + + def __init__(self, *, client_config: HTTPClientConfiguration | None = None): + self._client_config = client_config + + async def send( + self, + request: HTTPRequest, + *, + request_config: HTTPRequestConfiguration | None = None, + ) -> _smithy_http_interfaces_HTTPResponse: + # Raise the exception with the request object to bypass actual request handling + raise TestHttpServiceError(request) + + +class ResponseTestHTTPClient: + """An asynchronous HTTP client solely for testing purposes.""" + + TIMEOUT_EXCEPTIONS = () + SUPPORTS_DUPLEX_STREAMING: bool = True + + def __init__( + self, + *, + client_config: HTTPClientConfiguration | None = None, + status: int = 200, + headers: list[tuple[str, str]] | None = None, + body: bytes = b"", + ): + self._client_config = client_config + self.status = status + self.fields = tuples_to_fields(headers or []) + self.body = body + + async def send( + self, + request: HTTPRequest, + *, + request_config: HTTPRequestConfiguration | None = None, + ) -> _smithy_http_HTTPResponse: + # Pre-construct the response from the request and return it + return _smithy_http_HTTPResponse( + status=self.status, fields=self.fields, body=AsyncBytesReader(self.body) + ) diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java index 4108bb30d..4ef300574 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java @@ -200,6 +200,10 @@ public final class RuntimeTypes { "aio.protocols", "AsyncRpcV2CborClientProtocol", SmithyPythonDependency.SMITHY_HTTP); + public static final Symbol RPC_V2_JSON_CLIENT_PROTOCOL = createSymbol( + "aio.protocols", + "AsyncRpcV2JsonClientProtocol", + SmithyPythonDependency.SMITHY_HTTP); // smithy_aws_core.aio.identity.static public static final Symbol STATIC_CREDENTIALS_RESOLVER = createSymbol( diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SchemaTraitFilterIndex.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SchemaTraitFilterIndex.java index 70ca7a5ef..8adfb510b 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SchemaTraitFilterIndex.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SchemaTraitFilterIndex.java @@ -72,7 +72,8 @@ @SmithyInternalApi public final class SchemaTraitFilterIndex implements KnowledgeIndex { - // smithy-python reads this at runtime via schema.traits.get(...) in rpcv2Cbor unit detection. + // smithy-python reads this at runtime via schema.traits.get(...) in rpcv2Cbor and rpcv2Json + // unit detection. private static final ShapeId ORIGINAL_SHAPE_ID = ShapeId.from("smithy.synthetic#originalShapeId"); // Traits a protocol/auth meta-trait references but that must still never reach a schema. @@ -97,7 +98,7 @@ public final class SchemaTraitFilterIndex implements KnowledgeIndex { InputTrait.ID, // runtime input-structure nullability (@input members optional) OutputTrait.ID, // runtime injects response_metadata on output structures TimestampFormatTrait.ID, // read per-member by smithy_http / smithy_aws_core serde - ORIGINAL_SHAPE_ID, // rpcv2Cbor unit detection + ORIGINAL_SHAPE_ID, // rpcv2Cbor and rpcv2Json unit detection ServiceTrait.ID, // service metadata, including sdkId used by package docs ContextParamTrait.ID, // endpoint parameter plans read these at runtime StaticContextParamsTrait.ID, diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ValueNodeVisitor.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ValueNodeVisitor.java index 96556445f..2e86f130c 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ValueNodeVisitor.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ValueNodeVisitor.java @@ -4,6 +4,7 @@ */ package software.amazon.smithy.python.codegen; +import java.math.BigDecimal; import java.util.Optional; import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.model.Model; @@ -98,6 +99,13 @@ public Void numberNode(NumberNode node) { writer.writeInline(CodegenUtils.getDatetimeConstructor(writer, parsed)); } else if (inputShape.isFloatShape() || inputShape.isDoubleShape()) { writer.writeInline("float($L)", node.getValue()); + } else if (inputShape.isBigDecimalShape()) { + // A float literal would lose precision past ~17 digits and produce the + // wrong type, so build the Decimal from the exact model text. + writer.addStdlibImport("decimal", "Decimal"); + var value = node.getValue(); + var text = value instanceof BigDecimal bd ? bd.toPlainString() : value.toString(); + writer.writeInline("Decimal($S)", text); } else { writer.writeInline("$L", node.getValue()); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/Rpcv2JsonIntegration.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/Rpcv2JsonIntegration.java new file mode 100644 index 000000000..7f98e5c1a --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/Rpcv2JsonIntegration.java @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.integrations; + +import java.util.List; +import software.amazon.smithy.python.codegen.generators.ProtocolGenerator; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class Rpcv2JsonIntegration implements PythonIntegration { + @Override + public List getProtocolGenerators() { + return List.of(new Rpcv2JsonProtocolGenerator()); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/Rpcv2JsonProtocolGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/Rpcv2JsonProtocolGenerator.java new file mode 100644 index 000000000..7f7ec42d7 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/Rpcv2JsonProtocolGenerator.java @@ -0,0 +1,87 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.integrations; + +import java.util.Set; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.protocol.traits.Rpcv2JsonTrait; +import software.amazon.smithy.python.codegen.ApplicationProtocol; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.HttpProtocolTestGenerator; +import software.amazon.smithy.python.codegen.RuntimeTypes; +import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.generators.ProtocolGenerator; +import software.amazon.smithy.python.codegen.generators.ProtocolSettingsField; +import software.amazon.smithy.python.codegen.writer.PythonWriter; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * rpcv2Json is an RPC protocol: HTTP binding traits are ignored, requests are + * {@code POST}ed to a fixed URI, and the http config is taken from the protocol trait + * itself. The runtime behavior lives in the {@code AsyncRpcV2JsonClientProtocol} Python + * class in {@code smithy-http}. + */ +@SmithyUnstableApi +public final class Rpcv2JsonProtocolGenerator implements ProtocolGenerator { + + // A defaulted member is generated as a plain non-nullable field carrying its default + // (e.g. `member: int = 0`), so a value left UNSET is indistinguishable from one set + // to the default by the time serialization runs, and the client always serializes + // top-level defaults. Every vector that expects those omitted from the body fails, + // and a clientOptional member cannot be left absent. Tracking explicitness is a + // shared model-generation change across every protocol; rpcv2Cbor and RestJson park + // the same set. + private static final Set TESTS_TO_SKIP = Set.of( + "RpcV2JsonRequestClientPopulatesDefaultValuesInInput", + "RpcV2JsonRequestClientSkipsTopLevelDefaultValuesInInput", + "RpcV2JsonRequestClientUsesExplicitlyProvidedMemberValuesOverDefaults", + "RpcV2JsonRequestClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional"); + + @Override + public ShapeId getProtocol() { + return Rpcv2JsonTrait.ID; + } + + @Override + public ApplicationProtocol getApplicationProtocol(GenerationContext context) { + ServiceShape service = context.settings().service(context.model()); + Rpcv2JsonTrait trait = service.expectTrait(Rpcv2JsonTrait.class); + ObjectNode config = ObjectNode.builder() + .withMember("http", ArrayNode.fromStrings(trait.getHttp())) + .withMember("eventStreamHttp", ArrayNode.fromStrings(trait.getEventStreamHttp())) + .build(); + return ApplicationProtocol.createDefaultHttpApplicationProtocol(config); + } + + @Override + public void initializeProtocol(GenerationContext context, PythonWriter writer) { + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("json")); + writer.write("$T(_PROTOCOL_SETTINGS)", RuntimeTypes.RPC_V2_JSON_CLIENT_PROTOCOL); + } + + @Override + public Set requiredProtocolSettings(GenerationContext context) { + // The RPC URI is /service/{service_target}/operation/{name}, and the codec + // resolves document discriminators against the service namespace. + return Set.of( + ProtocolSettingsField.NAMESPACE, + ProtocolSettingsField.SERVICE_TARGET); + } + + @Override + public void generateProtocolTests(GenerationContext context) { + context.writerDelegator() + .useFileWriter("./tests/test_rpcv2json_protocol.py", "tests.test_rpcv2json_protocol", writer -> { + new HttpProtocolTestGenerator( + context, + getProtocol(), + writer, + (shape, testCase) -> TESTS_TO_SKIP.contains(testCase.getId())).run(); + }); + } +} diff --git a/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration b/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration index ccdf63695..d73c8e8b3 100644 --- a/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration +++ b/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration @@ -5,6 +5,7 @@ software.amazon.smithy.python.codegen.integrations.RestJsonIntegration software.amazon.smithy.python.codegen.integrations.Rpcv2CborIntegration +software.amazon.smithy.python.codegen.integrations.Rpcv2JsonIntegration software.amazon.smithy.python.codegen.integrations.HttpApiKeyAuth software.amazon.smithy.python.codegen.integrations.UserAgentIntegration software.amazon.smithy.python.codegen.integrations.EndpointBddIntegration diff --git a/codegen/protocol-test/model/my-handwritten-json-model.smithy b/codegen/protocol-test/model/my-handwritten-json-model.smithy new file mode 100644 index 000000000..398ee90c4 --- /dev/null +++ b/codegen/protocol-test/model/my-handwritten-json-model.smithy @@ -0,0 +1,89 @@ +$version: "2.0" + +namespace smithy.python.tests.handwritten.json + +use smithy.protocols#rpcv2Json + +/// The rpcv2Json counterpart of smithy.python.tests.handwritten#MyHandwrittenModel, +/// used to drive the duplex event-stream integration test over rpcv2Json. Shapes +/// are local copies (not shared with the CBOR service) so each service's shape +/// closure, default namespace, and generated client stay independent. +@rpcv2Json( + http: ["h2", "http/1.1"] + eventStreamHttp: ["h2"] +) +@title("My Handwritten JSON Model") +service MyHandwrittenJsonModel { + version: "2020-07-14" + operations: [ + TradeEventStream + ] +} + +/// Bidirectional (duplex) stream. The non-stream members (sessionId, greeting) are +/// carried as the initial-request and initial-response messages. +operation TradeEventStream { + input := { + sessionId: String + events: TradeEvents + } + output := { + greeting: String + events: TradeEvents + } + errors: [ + StreamServiceError + ] +} + +@streaming +union TradeEvents { + alpha: Alpha + + beta: Unit + + gamma: Gamma + + // The :event-type header must use the member name `delta`, not the shape name. + delta: DifferentShapeName + + throttle: ThrottlingEvent +} + +structure Alpha { + id: String + timestamp: Timestamp +} + +structure Gamma { + @eventHeader + sequenceNumber: Integer + + @eventPayload + payload: GammaPayload +} + +structure GammaPayload { + message: String + values: IntegerList +} + +structure DifferentShapeName { + name: String + number: Integer +} + +@error("client") +@retryable(throttling: true) +structure ThrottlingEvent { + message: String +} + +list IntegerList { + member: Integer +} + +@error("server") +structure StreamServiceError { + message: String +} diff --git a/codegen/protocol-test/smithy-build.json b/codegen/protocol-test/smithy-build.json index 555995439..d61acd2ba 100644 --- a/codegen/protocol-test/smithy-build.json +++ b/codegen/protocol-test/smithy-build.json @@ -199,6 +199,28 @@ } } }, + "rpcv2-json": { + "transforms": [ + { + "name": "includeServices", + "args": { + "services": [ + "smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol" + ] + } + }, + { + "name": "removeUnusedShapes" + } + ], + "plugins": { + "python-client-codegen": { + "service": "smithy.protocoltests.rpcv2Json#RpcV2JsonProtocol", + "module": "rpcv2json", + "moduleVersion": "0.0.1" + } + } + }, "query-compatible-rpcv2-cbor": { "transforms": [ { @@ -264,6 +286,28 @@ "moduleVersion": "0.0.1" } } + }, + "my-handwritten-json-model": { + "transforms": [ + { + "name": "includeServices", + "args": { + "services": [ + "smithy.python.tests.handwritten.json#MyHandwrittenJsonModel" + ] + } + }, + { + "name": "removeUnusedShapes" + } + ], + "plugins": { + "python-client-codegen": { + "service": "smithy.python.tests.handwritten.json#MyHandwrittenJsonModel", + "module": "my_handwritten_json_model", + "moduleVersion": "0.0.1" + } + } } } } diff --git a/packages/smithy-aws-core/tests/integration/_h2_server.py b/packages/smithy-aws-core/tests/integration/_h2_server.py index 708ffee02..f14a8078e 100644 --- a/packages/smithy-aws-core/tests/integration/_h2_server.py +++ b/packages/smithy-aws-core/tests/integration/_h2_server.py @@ -16,17 +16,22 @@ from smithy_aws_event_stream.events import Event, EventMessage EVENT_STREAM_CONTENT_TYPE = "application/vnd.amazon.eventstream" +# The defaults speak rpcv2Cbor; pass ``payload_content_type`` / ``smithy_protocol`` +# to H2BidiServer to speak another rpcv2 protocol (e.g. rpcv2Json). +CBOR_CONTENT_TYPE = "application/cbor" # rpcv2 responses must echo the request's Smithy-Protocol header; clients treat a -# response without it as malformed. This harness only speaks rpcv2Cbor. +# response without it as malformed. SMITHY_PROTOCOL = "rpc-v2-cbor" -def encode_event(event_type: str, payload: bytes) -> bytes: +def encode_event( + event_type: str, payload: bytes, content_type: str = CBOR_CONTENT_TYPE +) -> bytes: return EventMessage( headers={ ":message-type": "event", ":event-type": event_type, - ":content-type": "application/cbor", + ":content-type": content_type, }, payload=payload, ).encode() @@ -42,13 +47,21 @@ def encode_event(event_type: str, payload: bytes) -> bytes: class ServerStream: """The server's writable side of one event stream, handed to the referee.""" - def __init__(self, conn: h2.connection.H2Connection, stream_id: int) -> None: + def __init__( + self, + conn: h2.connection.H2Connection, + stream_id: int, + content_type: str = CBOR_CONTENT_TYPE, + ) -> None: self._conn = conn self._stream_id = stream_id + self._content_type = content_type def send(self, event_type: str, payload: bytes) -> None: self._conn.send_data( - self._stream_id, encode_event(event_type, payload), end_stream=False + self._stream_id, + encode_event(event_type, payload, self._content_type), + end_stream=False, ) def end(self) -> None: @@ -62,11 +75,24 @@ class H2BidiServer: and thereafter handed each client event frame, deciding what to send back. The ``initial-request`` itself is recorded in ``received_event_types`` but not passed to the referee -- it is request setup, not a game move. ``received_event_types`` - records every client frame's ``:event-type`` for post-hoc assertions. + records every client frame's ``:event-type`` for post-hoc assertions, and + ``received_content_types`` its ``:content-type``. + + ``payload_content_type`` is the ``:content-type`` of the event frames the server + sends, and ``smithy_protocol`` the ``Smithy-Protocol`` response header value; both + default to rpcv2Cbor. """ - def __init__(self, referee: Referee) -> None: + def __init__( + self, + referee: Referee, + *, + payload_content_type: str = CBOR_CONTENT_TYPE, + smithy_protocol: str = SMITHY_PROTOCOL, + ) -> None: self._referee = referee + self._payload_content_type = payload_content_type + self._smithy_protocol = smithy_protocol self._ca = trustme.CA() self._server_cert = self._ca.issue_cert("localhost", "127.0.0.1") self._ca_cert_ctx = self._ca.cert_pem.tempfile() @@ -76,6 +102,7 @@ def __init__(self, referee: Referee) -> None: self._server: asyncio.base_events.Server | None = None self._tasks: set[asyncio.Task[None]] = set() self.received_event_types: list[str] = [] + self.received_content_types: list[str] = [] async def __aenter__(self) -> "H2BidiServer": ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) @@ -115,25 +142,35 @@ async def _handle( decoder = _FrameDecoder() stream: ServerStream | None = None done = False + client_ended = False try: - while not writer.is_closing() and not done: + # Keep the connection up after the game ends until the client half-closes + # its side, so the client's own end-of-stream lands on a live stream. + while not writer.is_closing() and not (done and client_ended): data = await reader.read(65536) if not data: break for h2_event in conn.receive_data(data): if isinstance(h2_event, h2.events.RequestReceived): - stream = ServerStream(conn, h2_event.stream_id) + stream = ServerStream( + conn, h2_event.stream_id, self._payload_content_type + ) self._respond_headers(conn, stream) + elif isinstance(h2_event, h2.events.StreamEnded): + client_ended = True elif isinstance(h2_event, h2.events.DataReceived): conn.acknowledge_received_data( len(h2_event.data), h2_event.stream_id ) - if stream is None: + if stream is None or done: continue for frame in decoder.feed(h2_event.data): event_type = str(frame.message.headers.get(":event-type")) self.received_event_types.append(event_type) + self.received_content_types.append( + str(frame.message.headers.get(":content-type")) + ) if event_type == "initial-request": # Request setup, not a move: open the game off it. done = await self._referee(None, stream) @@ -159,7 +196,7 @@ def _respond_headers( [ (":status", "200"), ("content-type", EVENT_STREAM_CONTENT_TYPE), - ("smithy-protocol", SMITHY_PROTOCOL), + ("smithy-protocol", self._smithy_protocol), ], ) diff --git a/packages/smithy-aws-core/tests/integration/_simon_says.py b/packages/smithy-aws-core/tests/integration/_simon_says.py new file mode 100644 index 000000000..8edb7bb76 --- /dev/null +++ b/packages/smithy-aws-core/tests/integration/_simon_says.py @@ -0,0 +1,197 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""A protocol-agnostic game of Simon Says, played over a duplex event stream. + +Drives a generated rpcv2 client over the real CRT HTTP/2 duplex transport against a +local HTTP/2 server (``_h2_server.H2BidiServer``) that referees the game. The test +modules supply the protocol: the generated client and its models, the payload codec, +the event payload content type, and the ``Smithy-Protocol`` header value. + +The game makes the bidirectional exchange prove its own thoroughness. The server +(Simon) streams a scripted sequence of command events; each command's payload either +begins with ``simon says `` or does not. The client (the player loop) must send a +*move* event echoing the action when and only when Simon said so, and a *pass* event +otherwise. Because every turn produces exactly one client frame, the exchange is +deterministic: the referee advances one command per client frame and asserts the +client obeyed. A client that never put its events on the wire, or obeyed the wrong +commands, fails -- which a single ``initial-request``-only assertion could not catch. +""" + +from collections.abc import AsyncIterator, Callable +from dataclasses import dataclass +from types import ModuleType +from typing import Any + +from smithy_aws_event_stream.events import Event +from smithy_core import URI +from smithy_core.codecs import Codec +from smithy_http.aio.crt import AWSCRTHTTPClient, AWSCRTHTTPClientConfig + +from ._h2_server import H2BidiServer, ServerStream + +# The scripted game. A well-behaved client MOVES (echoes the action) on the commands +# Simon ordered and PASSES on the rest. The action is the command text with any +# "simon says " prefix stripped. +SIMON_SAYS = "simon says " +SCRIPT: list[str] = [ + "simon says touch your nose", + "jump", + "simon says raise your left hand", + "simon says spin around", + "clap twice", + "sit down", + "simon says take one step forward", +] + + +@dataclass(frozen=True) +class GameProtocol: + """Everything protocol-specific the game needs. + + ``models`` is the generated ``.models`` module, which must expose the + duplex ``TradeEventStream`` shapes (``Alpha``, ``Unit``, ``TradeEventsAlpha``, + ``TradeEventsBeta``, ``TradeEventStreamInput``, ``TradeEventStreamOutput``). + ``codec`` is what the referee encodes and decodes event payloads with; it must be + configured exactly as the client protocol's payload codec. When ``None``, the + referee uses the constructed client's own ``config.protocol.payload_codec``. + """ + + client_class: Any + config_class: Any + models: ModuleType + payload_content_type: str + smithy_protocol: str + codec: Codec | None = None + + +def action(command: str) -> str: + return command.removeprefix(SIMON_SAYS) + + +def ordered(command: str) -> bool: + return command.startswith(SIMON_SAYS) + + +def expected_moves() -> list[str]: + return [action(cmd) if ordered(cmd) else "" for cmd in SCRIPT] + + +def simon_referee( + models: ModuleType, codec: "Callable[[], Codec]" +) -> "tuple[Any, list[str]]": + """Build the server-side referee and the list it records the client's moves in. + + The referee sends the initial-response, then one command per turn, validating the + client's reply to the previous command before issuing the next. ``moves`` ends up + holding one entry per turn: the moved action, or ``""``. ``codec`` is + resolved on each turn, so it may be bound after the server is started. + """ + moves: list[str] = [] + state = {"turn": -1} + + async def referee(frame: Event | None, stream: ServerStream) -> bool: + turn = state["turn"] + payload_codec = codec() + + if frame is None: + # Open the game: initial-response, then the first command. + stream.send( + "initial-response", + payload_codec.serialize( + models.TradeEventStreamOutput(greeting="lets play") + ), + ) + state["turn"] = 0 + stream.send("alpha", payload_codec.serialize(models.Alpha(id=SCRIPT[0]))) + return False + + # A client frame is the reply to command `turn`. alpha = a move carrying the + # action id; beta = a pass. Record it, then issue the next command or finish. + event_type = str(frame.message.headers.get(":event-type")) + if event_type == "alpha": + deser = payload_codec.create_deserializer(frame.message.payload) + moves.append(models.Alpha.deserialize(deser).id or "") + elif event_type == "beta": + moves.append("") + else: + moves.append(f"") + + turn += 1 + state["turn"] = turn + if turn >= len(SCRIPT): + stream.end() + return True + stream.send("alpha", payload_codec.serialize(models.Alpha(id=SCRIPT[turn]))) + return False + + return referee, moves + + +async def play_simon_says(protocol: GameProtocol) -> None: + """Play one full game and assert the client obeyed every command.""" + models = protocol.models + bound: dict[str, Codec] = {} + if protocol.codec is not None: + bound["codec"] = protocol.codec + referee, moves = simon_referee(models, lambda: bound["codec"]) + async with H2BidiServer( + referee, + payload_content_type=protocol.payload_content_type, + smithy_protocol=protocol.smithy_protocol, + ) as server: + transport = AWSCRTHTTPClient( + client_config=AWSCRTHTTPClientConfig( + force_http_2=True, trust_store_path=str(server.ca_cert_path) + ) + ) + config = protocol.config_class( + endpoint_uri=URI(host=server.host, port=server.port, scheme="https"), + transport=transport, + ) + # Default to the codec the client itself will use for event payloads. + bound.setdefault("codec", config.protocol.payload_codec) + client = protocol.client_class(config) + + stream = await client.trade_event_stream( + models.TradeEventStreamInput(session_id="player-1") + ) + output, output_stream = await stream.await_output() + assert output.greeting == "lets play" + + # Player loop: for each command Simon streams, move iff ordered, else pass. + async for command in _commands(protocol, output_stream): + if ordered(command): + await stream.input_stream.send( + models.TradeEventsAlpha(value=models.Alpha(id=action(command))) + ) + else: + await stream.input_stream.send( + models.TradeEventsBeta(value=models.Unit()) + ) + await stream.input_stream.close() + + await transport.close() + + expected = expected_moves() + assert moves == expected + # Every ordered command was obeyed and no pass leaked a move onto the wire. + assert [m for m in moves if m != ""] == [ + action(cmd) for cmd in SCRIPT if ordered(cmd) + ] + # The server saw the initial-request, then exactly one move/pass frame per turn. + assert server.received_event_types == ["initial-request"] + [ + "alpha" if ordered(cmd) else "beta" for cmd in SCRIPT + ] + # Every frame -- including the empty-structure passes -- was encoded with the + # protocol's codec. + assert server.received_content_types == [protocol.payload_content_type] * len( + server.received_event_types + ) + + +async def _commands( + protocol: GameProtocol, output_stream: AsyncIterator[object] +) -> AsyncIterator[str]: + async for event in output_stream: + assert isinstance(event, protocol.models.TradeEventsAlpha) + yield event.value.id or "" diff --git a/packages/smithy-aws-core/tests/integration/test_rpcv2cbor_event_stream.py b/packages/smithy-aws-core/tests/integration/test_rpcv2cbor_event_stream.py index c055141a3..7deb8150c 100644 --- a/packages/smithy-aws-core/tests/integration/test_rpcv2cbor_event_stream.py +++ b/packages/smithy-aws-core/tests/integration/test_rpcv2cbor_event_stream.py @@ -14,9 +14,10 @@ deterministic: the referee advances one command per client frame and asserts the client obeyed. A client that never put its events on the wire, or obeyed the wrong commands, fails -- which a single ``initial-request``-only assertion could not catch. -""" -from collections.abc import AsyncIterator +The game itself lives in ``_simon_says`` and is shared with the rpcv2Json variant +(``test_rpcv2json_event_stream``). +""" import pytest @@ -26,145 +27,22 @@ pytest.importorskip("h2") pytest.importorskip("trustme") +import my_handwritten_model.models from my_handwritten_model.aio.client import AsyncMyHandwrittenModel from my_handwritten_model.aio.config import AsyncConfig -from my_handwritten_model.models import ( - Alpha, - TradeEventsAlpha, - TradeEventsBeta, - TradeEventStreamInput, - TradeEventStreamOutput, - Unit, -) -from smithy_aws_event_stream.events import Event from smithy_cbor import CBORCodec -from smithy_core import URI -from smithy_http.aio.crt import ( - AWSCRTHTTPClient, - AWSCRTHTTPClientConfig, -) - -from ._h2_server import H2BidiServer, ServerStream - -_CODEC = CBORCodec() - -# The scripted game. Each entry is (command text, whether Simon ordered it). A -# well-behaved client MOVES (echoes the action) on the ordered ones and PASSES on -# the rest. The action is the command text with any "simon says " prefix stripped. -_SIMON_SAYS = "simon says " -_SCRIPT: list[str] = [ - "simon says touch your nose", - "jump", - "simon says raise your left hand", - "simon says spin around", - "clap twice", - "sit down", - "simon says take one step forward", -] - - -def _action(command: str) -> str: - return command.removeprefix(_SIMON_SAYS) - - -def _ordered(command: str) -> bool: - return command.startswith(_SIMON_SAYS) - - -def _simon_referee() -> "tuple[object, list[str]]": - """Build the server-side referee and the list it records the client's moves in. - The referee sends the initial-response, then one command per turn, validating the - client's reply to the previous command before issuing the next. ``moves`` ends up - holding one entry per turn: the moved action, or ``""``. - """ - moves: list[str] = [] - state = {"turn": -1} +from ._simon_says import GameProtocol, play_simon_says - async def referee(frame: Event | None, stream: ServerStream) -> bool: - turn = state["turn"] - - if frame is None: - # Open the game: initial-response, then the first command. - stream.send( - "initial-response", - _CODEC.serialize(shape=TradeEventStreamOutput(greeting="lets play")), - ) - state["turn"] = 0 - stream.send("alpha", _CODEC.serialize(shape=Alpha(id=_SCRIPT[0]))) - return False - - # A client frame is the reply to command `turn`. alpha = a move carrying the - # action id; beta = a pass. Record it, then issue the next command or finish. - event_type = str(frame.message.headers.get(":event-type")) - if event_type == "alpha": - deser = _CODEC.create_deserializer(frame.message.payload) - moves.append(Alpha.deserialize(deser).id or "") - elif event_type == "beta": - moves.append("") - else: - moves.append(f"") - - turn += 1 - state["turn"] = turn - if turn >= len(_SCRIPT): - stream.end() - return True - stream.send("alpha", _CODEC.serialize(shape=Alpha(id=_SCRIPT[turn]))) - return False - - return referee, moves - - -async def _client( - server: H2BidiServer, -) -> tuple[AsyncMyHandwrittenModel, AWSCRTHTTPClient]: - transport = AWSCRTHTTPClient( - client_config=AWSCRTHTTPClientConfig( - force_http_2=True, trust_store_path=str(server.ca_cert_path) - ) - ) - client = AsyncMyHandwrittenModel( - AsyncConfig( - endpoint_uri=URI(host=server.host, port=server.port, scheme="https"), - transport=transport, - ) - ) - return client, transport +_RPCV2_CBOR = GameProtocol( + client_class=AsyncMyHandwrittenModel, + config_class=AsyncConfig, + models=my_handwritten_model.models, + codec=CBORCodec(), + payload_content_type="application/cbor", + smithy_protocol="rpc-v2-cbor", +) async def test_simon_says_duplex() -> None: - referee, moves = _simon_referee() - async with H2BidiServer(referee) as server: - client, transport = await _client(server) - - stream = await client.trade_event_stream( - TradeEventStreamInput(session_id="player-1") - ) - output, output_stream = await stream.await_output() - assert output.greeting == "lets play" - - # Player loop: for each command Simon streams, move iff ordered, else pass. - async for command in _commands(output_stream): - if _ordered(command): - await stream.input_stream.send( - TradeEventsAlpha(value=Alpha(id=_action(command))) - ) - else: - await stream.input_stream.send(TradeEventsBeta(value=Unit())) - await stream.input_stream.close() - - await transport.close() - - expected = [_action(cmd) if _ordered(cmd) else "" for cmd in _SCRIPT] - assert moves == expected - # Every ordered command was obeyed and no pass leaked a move onto the wire. - assert [m for m in moves if m != ""] == [ - _action(cmd) for cmd in _SCRIPT if _ordered(cmd) - ] - - -async def _commands(output_stream: AsyncIterator[object]) -> AsyncIterator[str]: - async for event in output_stream: - assert isinstance(event, TradeEventsAlpha) - yield event.value.id or "" + await play_simon_says(_RPCV2_CBOR) diff --git a/packages/smithy-aws-core/tests/integration/test_rpcv2json_event_stream.py b/packages/smithy-aws-core/tests/integration/test_rpcv2json_event_stream.py new file mode 100644 index 000000000..558a2575c --- /dev/null +++ b/packages/smithy-aws-core/tests/integration/test_rpcv2json_event_stream.py @@ -0,0 +1,66 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""End-to-end duplex event streaming against the generated rpcv2Json client. + +The rpcv2Json counterpart of ``test_rpcv2cbor_event_stream``: drives the generated +local client (``codegen-output/my-handwritten-json-model``) over the real CRT HTTP/2 +duplex transport against a local HTTP/2 server (``_h2_server.H2BidiServer``) that +referees a game of Simon Says. + +The server (Simon) streams a scripted sequence of command events; the client must +send a *move* event echoing the action when and only when the command begins with +``simon says ``, and a *pass* event otherwise. Every turn produces exactly one client +frame, so the referee can advance one command per client frame and assert the client +obeyed. See ``_simon_says`` for the shared game. +""" + +import pytest + +pytest.importorskip("smithy_json") +pytest.importorskip("my_handwritten_json_model") +pytest.importorskip("awscrt") +pytest.importorskip("h2") +pytest.importorskip("trustme") + +import my_handwritten_json_model.models +from my_handwritten_json_model.aio.client import AsyncMyHandwrittenJsonModel +from my_handwritten_json_model.aio.config import AsyncConfig +from smithy_core.types import TimestampFormat +from smithy_http.aio.crt import AWSCRTHTTPClient +from smithy_http.aio.protocols import AsyncRpcV2JsonClientProtocol +from smithy_json import JSONCodec + +from ._simon_says import GameProtocol, play_simon_says + +# codec=None: the referee encodes and decodes event payloads with the client's own +# protocol payload codec, so it cannot drift from what the client puts on the wire. +_RPCV2_JSON = GameProtocol( + client_class=AsyncMyHandwrittenJsonModel, + config_class=AsyncConfig, + models=my_handwritten_json_model.models, + payload_content_type="application/json", + smithy_protocol="rpc-v2-json", +) + + +async def test_client_uses_rpcv2json_codec() -> None: + # The codec the referee borrows must be the rpcv2Json-configured JSONCodec. + transport = AWSCRTHTTPClient() + try: + protocol = AsyncConfig(transport=transport).protocol + assert isinstance(protocol, AsyncRpcV2JsonClientProtocol) + codec = protocol.payload_codec + assert isinstance(codec, JSONCodec) + assert codec.media_type == "application/json" + settings = codec._settings # pyright: ignore[reportPrivateUsage] + assert settings.use_json_name is False + assert settings.use_timestamp_format is False + assert settings.default_timestamp_format is TimestampFormat.EPOCH_SECONDS + assert settings.arbitrary_precision_as_string is True + assert settings.default_namespace == "smithy.python.tests.handwritten.json" + finally: + await transport.close() + + +async def test_simon_says_duplex() -> None: + await play_simon_says(_RPCV2_JSON)