Skip to content

Commit a493125

Browse files
Add design doc and a few generator tweaks
1 parent 4c4cdf8 commit a493125

File tree

4 files changed

+156
-9
lines changed

4 files changed

+156
-9
lines changed

codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import software.amazon.smithy.model.traits.InputTrait;
3232
import software.amazon.smithy.model.traits.OutputTrait;
3333
import software.amazon.smithy.model.traits.RequiredTrait;
34+
import software.amazon.smithy.model.traits.RetryableTrait;
3435
import software.amazon.smithy.model.traits.SensitiveTrait;
3536
import software.amazon.smithy.model.traits.StreamingTrait;
3637
import software.amazon.smithy.python.codegen.CodegenUtils;
@@ -134,12 +135,26 @@ private void renderError() {
134135
var symbol = symbolProvider.toSymbol(shape);
135136
var baseError = CodegenUtils.getServiceError(settings);
136137
writer.pushState(new ErrorSection(symbol));
138+
writer.putContext("retryable", false);
139+
writer.putContext("throttling", false);
140+
141+
var retryableTrait = shape.getTrait(RetryableTrait.class);
142+
if (retryableTrait.isPresent()) {
143+
writer.putContext("retryable", true);
144+
writer.putContext("throttling", retryableTrait.get().getThrottling());
145+
}
137146
writer.write("""
138147
@dataclass(kw_only=True)
139148
class $1L($2T):
140149
${4C|}
141150
142151
fault: Literal["client", "server"] | None = $3S
152+
${?retryable}
153+
is_retry_safe: bool | None = True
154+
${?throttling}
155+
is_throttle: bool = True
156+
${/throttling}
157+
${/retryable}
143158
144159
${5C|}
145160

codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpProtocolGeneratorUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public static void generateErrorDispatcher(
140140
delegator.useFileWriter(errorDispatcher.getDefinitionFile(), errorDispatcher.getNamespace(), writer -> {
141141
writer.pushState(new ErrorDispatcherSection(operation, errorShapeToCode, errorMessageCodeGenerator));
142142
writer.addImport("smithy_core.exceptions", "CallException");
143+
// TODO: include standard retry-after in the pure-python version of this
143144
writer.write("""
144145
async def $1L(http_response: $2T, config: $3T) -> CallException:
145146
${4C|}
@@ -148,9 +149,12 @@ public static void generateErrorDispatcher(
148149
${5C|}
149150
150151
case _:
152+
is_throttle = http_response.status == 429
151153
return CallException(
152154
message=f"{code}: {message}",
153-
fault="client" if http_response.status < 500 else "server"
155+
fault="client" if http_response.status < 500 else "server",
156+
is_throttle=is_throttle,
157+
is_retry_safe=is_throttle or None,
154158
)
155159
""",
156160
errorDispatcher.getName(),

designs/exceptions.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Exceptions
2+
3+
Exceptions are a necessary aspect of any software product (Go notwithstanding),
4+
and care must be taken in how they're exposed. This document describes how
5+
smithy-python clients will expose exceptions to customers.
6+
7+
## Goals
8+
9+
* Every exception raised by a Smithy client should be catchable with a single,
10+
specific catch statement (that is, not just `except Exception`).
11+
* Every modeled exception raised by a service should be catchable with a single,
12+
specific catch statement.
13+
* Exceptions should contain information about retryablility where relevant.
14+
15+
## Specification
16+
17+
Every exception raised by a Smithy client MUST inherit from `SmithyException`.
18+
19+
```python
20+
class SmithyException(Exception):
21+
"""Base exception type for all exceptions raised by smithy-python."""
22+
```
23+
24+
If an exception that is not a `SmithyException` is thrown while executing a
25+
request, that exception MUST be wrapped in a `SmithyException` and the
26+
`__cause__` MUST be set to the original exception.
27+
28+
Just as in normal Python programming, different exception types SHOULD be made
29+
for different kinds of exceptions. `SerializationException`, for example, will
30+
serve as the exception type for any exceptions that occur while serializing a
31+
request.
32+
33+
### Retryability
34+
35+
Not all exceptions need to include information about retryability, as most will
36+
not be retryable at all. To avoid overly complicating the class hierarchy,
37+
retryability properties will be standardized as a `Protocol` that exceptions MAY
38+
implement.
39+
40+
```python
41+
@dataclass(kw_only=True)
42+
@runtime_checkable
43+
class RetryInfo(Protocol):
44+
is_retry_safe: bool | None = None
45+
"""Whether the exception is safe to retry.
46+
47+
A value of True does not mean a retry will occur, but rather that a retry is allowed
48+
to occur.
49+
50+
A value of None indicates that there is not enough information available to
51+
determine if a retry is safe.
52+
"""
53+
54+
retry_after: float | None = None
55+
"""The amount of time that should pass before a retry.
56+
57+
Retry strategies MAY choose to wait longer.
58+
"""
59+
60+
is_throttle: bool = False
61+
"""Whether the error is a throttling error."""
62+
```
63+
64+
If an exception with `RetryInfo` is received while attempting to send a
65+
serialized request to the server, the contained information will be used to
66+
inform the next retry.
67+
68+
### Service Exceptions
69+
70+
Exceptions returned by the service MUST be a `CallException`. `CallException`s
71+
include a `fault` property that indicates whether the client or server is
72+
responsible for the exception. HTTP protocols can determine this based on the
73+
status code.
74+
75+
Similarly, protocols can and should determine retry information. HTTP protocols
76+
can generally be confident that a status code 429 is a throttling error and can
77+
also make use of the `Retry-After` header. Specific protocols may also include
78+
more information in protocol-specific headers.
79+
80+
```python
81+
type Fault = Literal["client", "server"] | None
82+
"""Whether the client or server is at fault.
83+
84+
If None, then there was not enough information to determine fault.
85+
"""
86+
87+
88+
@dataclass(kw_only=True)
89+
class CallException(SmithyException, RetryInfo):
90+
fault: Fault = None
91+
message: str = field(default="", kw_only=False)
92+
```
93+
94+
#### Modeled Exceptions
95+
96+
Most exceptions thrown by a service will be present in the Smithy model for the
97+
service. These exceptions will all be generated into the client package. Each
98+
modeled exception will be inherit from a generated exception named
99+
`ServiceException` which itself inherits from the static `ModeledException`.
100+
101+
```python
102+
@dataclass(kw_only=True)
103+
class ModeledException(CallException):
104+
"""Base exception to be used for modeled errors."""
105+
```
106+
107+
The Smithy model itself can contain fault information in the
108+
[error trait](https://smithy.io/2.0/spec/type-refinement-traits.html#smithy-api-error-trait)
109+
and retry information in the
110+
[retryable trait](https://smithy.io/2.0/spec/behavior-traits.html#retryable-trait).
111+
This information will be statically generated onto the exception.
112+
113+
```python
114+
@dataclass(kw_only=True)
115+
class ServiceException(ModeledException):
116+
pass
117+
118+
119+
@dataclass(kw_only=True)
120+
class ThrottlingException(ServcieException):
121+
fault: Fault = "client"
122+
is_retry_safe: bool | None = True
123+
is_throttle: bool = True
124+
```

packages/smithy-core/src/smithy_core/exceptions.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
from dataclasses import dataclass, field
4-
from typing import Literal, Protocol
4+
from typing import Literal, Protocol, runtime_checkable
55

66

77
class SmithyException(Exception):
88
"""Base exception type for all exceptions raised by smithy-python."""
99

1010

11+
type Fault = Literal["client", "server"] | None
12+
"""Whether the client or server is at fault.
13+
14+
If None, then there was not enough information to determine fault.
15+
"""
16+
17+
1118
@dataclass(kw_only=True)
19+
@runtime_checkable
1220
class RetryInfo(Protocol):
1321
is_retry_safe: bool | None = None
1422
"""Whether the exception is safe to retry.
@@ -32,9 +40,9 @@ class RetryInfo(Protocol):
3240

3341
@dataclass(kw_only=True)
3442
class CallException(SmithyException, RetryInfo):
35-
"""Base exceptio to be used in application-level errors."""
43+
"""Base exception to be used in application-level errors."""
3644

37-
fault: Literal["client", "server"] | None = None
45+
fault: Fault = None
3846
"""Whether the client or server is at fault.
3947
4048
If None, then there was not enough information to determine fault.
@@ -51,11 +59,7 @@ def __post_init__(self):
5159
class ModeledException(CallException):
5260
"""Base exception to be used for modeled errors."""
5361

54-
fault: Literal["client", "server"] | None = "client"
55-
"""Whether the client or server is at fault.
56-
57-
If None, then there was not enough information to determine fault.
58-
"""
62+
fault: Fault = "client"
5963

6064

6165
class SerializationException(Exception):

0 commit comments

Comments
 (0)