Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ async def _handle_attempt(
endpoint = await config.endpoint_resolver.resolve_endpoint(
StaticEndpointParams(url=config.endpoint_url)
)
if not endpoint.url.path:
endpoint.url.path = ""
elif endpoint.url.path.endswith("/"):
endpoint.url.path = endpoint.url.path[:-1]
if context.transport_request.url.path:
endpoint.url.path += context.transport_request.url.path
endpoint.url.query = context.transport_request.url.query
endpoint.url.host = context.transport_request.url.host + endpoint.url.host
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what's happening here. Why are we concatenating two hosts and what's the expected outcome? Is context.transport_request.url.host a prefix in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the hostLabel trait allows for setting an endpoint prefix, so this is just accounting for that.

context._transport_request.url = endpoint.url
context._transport_request.headers.extend(endpoint.headers)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,28 @@
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.ErrorTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.StringUtils;

Expand Down Expand Up @@ -209,4 +220,72 @@ public static List<Object[]> toTuples(Map<?, ?> map) {
return map.entrySet().stream().map((entry) ->
List.of(entry.getKey(), entry.getValue()).toArray()).toList();
}

/**
* Generates a Python datetime constructor for the given ZonedDateTime.
*
* @param writer A writer to add dependencies to.
* @param value The ZonedDateTime to convert.
* @return A string containing a Python datetime constructor representing the given ZonedDateTime.
*/
public static String getDatetimeConstructor(PythonWriter writer, ZonedDateTime value) {
writer.addStdlibImport("datetime", "datetime");
writer.addStdlibImport("datetime", "timezone");
var timezone = "timezone.utc";
if (value.getOffset() != ZoneOffset.UTC) {
writer.addStdlibImport("datetime", "timedelta");
timezone = String.format("timezone(timedelta(seconds=%d))", value.getOffset().getTotalSeconds());
}
return String.format("datetime(%d, %d, %d, %d, %d, %d, %d, %s)", value.get(ChronoField.YEAR),
value.get(ChronoField.MONTH_OF_YEAR), value.get(ChronoField.DAY_OF_MONTH),
value.get(ChronoField.HOUR_OF_DAY), value.get(ChronoField.MINUTE_OF_HOUR),
value.get(ChronoField.SECOND_OF_MINUTE), value.get(ChronoField.MICRO_OF_SECOND), timezone);
}

/**
* Parses a timestamp Node.
*
* <p>This is used to offload modeled timestamp parsing from Python runtime to Java build time.
*
* @param model The model being generated.
* @param shape The shape of the node.
* @param value The node to parse.
* @return A parsed ZonedDateTime representation of the given node.
*/
public static ZonedDateTime parseTimestampNode(Model model, Shape shape, Node value) {
if (value.isNumberNode()) {
return parseEpochTime(value);
}

Optional<TimestampFormatTrait> trait = shape.getTrait(TimestampFormatTrait.class);
if (shape.isMemberShape()) {
trait = shape.asMemberShape().get().getMemberTrait(model, TimestampFormatTrait.class);
}
var format = Format.DATE_TIME;
if (trait.isPresent()) {
format = trait.get().getFormat();
}

return switch (format) {
case DATE_TIME -> parseDateTime(value);
case HTTP_DATE -> parseHttpDate(value);
default -> throw new CodegenException("Unexpected timestamp format: " + format);
};
}

private static ZonedDateTime parseEpochTime(Node value) {
Number number = value.expectNumberNode().getValue();
Instant instant = Instant.ofEpochMilli(Double.valueOf(number.doubleValue() * 1000).longValue());
return instant.atZone(ZoneId.of("UTC"));
}

private static ZonedDateTime parseDateTime(Node value) {
Instant instant = Instant.from(DateTimeFormatter.ISO_INSTANT.parse(value.expectStringNode().getValue()));
return instant.atZone(ZoneId.of("UTC"));
}

private static ZonedDateTime parseHttpDate(Node value) {
Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(value.expectStringNode().getValue()));
return instant.atZone(ZoneId.of("UTC"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,8 @@ public Void nullNode(NullNode node) {
public Void numberNode(NumberNode node) {
// TODO: Add support for timestamp, int-enum, and others
if (inputShape.isTimestampShape()) {
writer.addStdlibImport("datetime", "datetime");
writer.writeInline("datetime.fromtimestamp($L)", node.getValue());
var parsed = CodegenUtils.parseTimestampNode(model, inputShape, node);
writer.writeInline(CodegenUtils.getDatetimeConstructor(writer, parsed));
} else if (inputShape.isFloatShape() || inputShape.isDoubleShape()) {
writer.writeInline("float($L)", node.getValue());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public final class SmithyPythonDependency {
*/
public static final PythonDependency PYTEST_ASYNCIO = new PythonDependency(
"pytest-asyncio",
">=0.2.3,<0.3.0",
">=0.20.3,<0.21.0",
Type.TEST_DEPENDENCY,
false
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,9 @@
import static java.lang.String.format;
import static software.amazon.smithy.python.codegen.CodegenUtils.isErrorMessage;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
Expand All @@ -43,7 +38,6 @@
import software.amazon.smithy.model.traits.ErrorTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.traits.SensitiveTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;


/**
Expand Down Expand Up @@ -299,15 +293,8 @@ private String getDefaultValue(PythonWriter writer, MemberShape member) {
var defaultNode = member.expectTrait(DefaultTrait.class).toNode();
var target = model.expectShape(member.getTarget());
if (target.isTimestampShape()) {
writer.addStdlibImport("datetime", "datetime");
writer.addStdlibImport("datetime", "timezone");
// We *could* let python do this parsing, but then that work has to be done every time a customer
// runs their code.
ZonedDateTime value = parseDefaultTimestamp(member, defaultNode);
return String.format("datetime(%d, %d, %d, %d, %d, %d, %d, timezone.utc)", value.get(ChronoField.YEAR),
value.get(ChronoField.MONTH_OF_YEAR), value.get(ChronoField.DAY_OF_MONTH),
value.get(ChronoField.HOUR_OF_DAY), value.get(ChronoField.MINUTE_OF_HOUR),
value.get(ChronoField.SECOND_OF_MINUTE), value.get(ChronoField.MICRO_OF_SECOND));
ZonedDateTime value = CodegenUtils.parseTimestampNode(model, member, defaultNode);
return CodegenUtils.getDatetimeConstructor(writer, value);
} else if (target.isBlobShape()) {
return String.format("b'%s'", defaultNode.expectStringNode().getValue());
}
Expand All @@ -318,46 +305,6 @@ private String getDefaultValue(PythonWriter writer, MemberShape member) {
};
}

private ZonedDateTime parseDefaultTimestamp(MemberShape member, Node value) {
Optional<TimestampFormatTrait> trait = member.getMemberTrait(model, TimestampFormatTrait.class);
if (trait.isPresent()) {
switch (trait.get().getFormat()) {
case EPOCH_SECONDS:
return parseEpochTime(value);
case DATE_TIME:
return parseDateTime(value);
case HTTP_DATE:
return parseHttpDate(value);
default:
break;
}
}

if (value.isNumberNode()) {
return parseEpochTime(value);
} else {
// Smithy's node validator asserts that string nodes are in the http date format if there
// is no format explicitly given.
return parseDateTime(value);
}
}

private ZonedDateTime parseEpochTime(Node value) {
Number number = value.expectNumberNode().getValue();
Instant instant = Instant.ofEpochMilli(Double.valueOf(number.doubleValue() * 1000).longValue());
return instant.atZone(ZoneId.of("UTC"));
}

private ZonedDateTime parseDateTime(Node value) {
Instant instant = Instant.from(DateTimeFormatter.ISO_INSTANT.parse(value.expectStringNode().getValue()));
return instant.atZone(ZoneId.of("UTC"));
}

private ZonedDateTime parseHttpDate(Node value) {
Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(value.expectStringNode().getValue()));
return instant.atZone(ZoneId.of("UTC"));
}

private boolean hasDocs() {
if (shape.hasTrait(DocumentationTrait.class)) {
return true;
Expand Down
Loading