Skip to content

Commit c93c717

Browse files
Decouple endpoint resolvers from http
1 parent 9cce158 commit c93c717

File tree

14 files changed

+203
-176
lines changed

14 files changed

+203
-176
lines changed

packages/smithy-aws-core/src/smithy_aws_core/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@
44
import importlib.metadata
55

66
__version__: str = importlib.metadata.version("smithy-aws-core")
7+
8+
9+
from smithy_core.types import PropertyKey
10+
11+
12+
REGION = PropertyKey(key="region", value_type=str)
13+
"""An AWS region."""

packages/smithy-aws-core/src/smithy_aws_core/endpoints/standard_regional.py

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,29 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
from dataclasses import dataclass
4-
from typing import Protocol, Self
5-
from urllib.parse import urlparse
3+
from typing import Any
64

7-
import smithy_core
85
from smithy_core import URI
9-
from smithy_http.aio.interfaces import (
10-
EndpointResolver,
11-
EndpointParameters,
12-
)
13-
from smithy_http.endpoints import Endpoint
14-
from smithy_http.exceptions import EndpointResolutionError
6+
from smithy_core.aio.interfaces import EndpointResolver
7+
from smithy_core.endpoints import Endpoint, EndpointResolverParams, resolve_static_uri
8+
from smithy_core.exceptions import EndpointResolutionError
159

10+
from .. import REGION
1611

17-
class _RegionUriConfig(Protocol):
18-
endpoint_uri: str | smithy_core.interfaces.URI | None
19-
region: str | None
2012

21-
22-
@dataclass(kw_only=True)
23-
class RegionalEndpointParameters(EndpointParameters[_RegionUriConfig]):
24-
"""Endpoint parameters for services with standard regional endpoints."""
25-
26-
sdk_endpoint: str | smithy_core.interfaces.URI | None
27-
region: str | None
28-
29-
@classmethod
30-
def build(cls, config: _RegionUriConfig) -> Self:
31-
return cls(sdk_endpoint=config.endpoint_uri, region=config.region)
32-
33-
34-
class StandardRegionalEndpointsResolver(EndpointResolver[RegionalEndpointParameters]):
13+
class StandardRegionalEndpointsResolver(EndpointResolver):
3514
"""Resolves endpoints for services with standard regional endpoints."""
3615

3716
def __init__(self, endpoint_prefix: str = "bedrock-runtime"):
3817
self._endpoint_prefix = endpoint_prefix
3918

40-
async def resolve_endpoint(self, params: RegionalEndpointParameters) -> Endpoint:
41-
if params.sdk_endpoint is not None:
42-
# If it's not a string, it's already a parsed URI so just pass it along.
43-
if not isinstance(params.sdk_endpoint, str):
44-
return Endpoint(uri=params.sdk_endpoint)
45-
46-
parsed = urlparse(params.sdk_endpoint)
47-
48-
# This will end up getting wrapped in the client.
49-
if parsed.hostname is None:
50-
raise EndpointResolutionError(
51-
f"Unable to parse hostname from provided URI: {params.sdk_endpoint}"
52-
)
53-
54-
return Endpoint(
55-
uri=URI(
56-
host=parsed.hostname,
57-
path=parsed.path,
58-
scheme=parsed.scheme,
59-
query=parsed.query,
60-
port=parsed.port,
61-
)
62-
)
19+
async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoint:
20+
if (static_uri := resolve_static_uri(params)) is not None:
21+
return Endpoint(uri=static_uri)
6322

64-
if params.region is not None:
23+
if (region := params.context.get(REGION)) is not None:
6524
# TODO: use dns suffix determined from partition metadata
6625
dns_suffix = "amazonaws.com"
67-
hostname = f"{self._endpoint_prefix}.{params.region}.{dns_suffix}"
26+
hostname = f"{self._endpoint_prefix}.{region}.{dns_suffix}"
6827

6928
return Endpoint(uri=URI(host=hostname))
7029

packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
from smithy_aws_core.endpoints.standard_regional import (
4-
StandardRegionalEndpointsResolver,
5-
RegionalEndpointParameters,
6-
)
7-
8-
from smithy_core import URI
3+
from unittest.mock import Mock
94

105
import pytest
116

12-
from smithy_http.exceptions import EndpointResolutionError
7+
from smithy_core import URI
8+
from smithy_core.endpoints import STATIC_URI, EndpointResolverParams
9+
from smithy_core.types import TypedProperties
10+
from smithy_core.exceptions import EndpointResolutionError
11+
12+
from smithy_aws_core import REGION
13+
from smithy_aws_core.endpoints.standard_regional import (
14+
StandardRegionalEndpointsResolver,
15+
)
1316

1417

1518
async def test_resolve_endpoint_with_valid_sdk_endpoint_string():
1619
resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service")
17-
params = RegionalEndpointParameters(
18-
sdk_endpoint="https://example.com/path?query=123", region=None
20+
params = Mock(spec=EndpointResolverParams)
21+
params.context = TypedProperties(
22+
{STATIC_URI.key: "https://example.com/path?query=123"}
1923
)
2024

2125
endpoint = await resolver.resolve_endpoint(params)
@@ -31,7 +35,8 @@ async def test_resolve_endpoint_with_sdk_endpoint_uri():
3135
parsed_uri = URI(
3236
host="example.com", path="/path", scheme="https", query="query=123", port=443
3337
)
34-
params = RegionalEndpointParameters(sdk_endpoint=parsed_uri, region=None)
38+
params = Mock(spec=EndpointResolverParams)
39+
params.context = TypedProperties({STATIC_URI.key: parsed_uri})
3540

3641
endpoint = await resolver.resolve_endpoint(params)
3742

@@ -40,15 +45,17 @@ async def test_resolve_endpoint_with_sdk_endpoint_uri():
4045

4146
async def test_resolve_endpoint_with_invalid_sdk_endpoint():
4247
resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service")
43-
params = RegionalEndpointParameters(sdk_endpoint="invalid-uri", region=None)
48+
params = Mock(spec=EndpointResolverParams)
49+
params.context = TypedProperties({STATIC_URI.key: "invalid_uri"})
4450

4551
with pytest.raises(EndpointResolutionError):
4652
await resolver.resolve_endpoint(params)
4753

4854

4955
async def test_resolve_endpoint_with_region():
5056
resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service")
51-
params = RegionalEndpointParameters(sdk_endpoint=None, region="us-west-2")
57+
params = Mock(spec=EndpointResolverParams)
58+
params.context = TypedProperties({REGION.key: "us-west-2"})
5259

5360
endpoint = await resolver.resolve_endpoint(params)
5461

@@ -57,16 +64,18 @@ async def test_resolve_endpoint_with_region():
5764

5865
async def test_resolve_endpoint_with_no_sdk_endpoint_or_region():
5966
resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service")
60-
params = RegionalEndpointParameters(sdk_endpoint=None, region=None)
67+
params = Mock(spec=EndpointResolverParams)
68+
params.context = TypedProperties()
6169

6270
with pytest.raises(EndpointResolutionError):
6371
await resolver.resolve_endpoint(params)
6472

6573

6674
async def test_resolve_endpoint_with_sdk_endpoint_and_region():
6775
resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service")
68-
params = RegionalEndpointParameters(
69-
sdk_endpoint="https://example.com", region="us-west-2"
76+
params = Mock(spec=EndpointResolverParams)
77+
params.context = TypedProperties(
78+
{STATIC_URI.key: "https://example.com", REGION.key: "us-west-2"}
7079
)
7180

7281
endpoint = await resolver.resolve_endpoint(params)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Any
4+
5+
from .interfaces import EndpointResolver
6+
from ..endpoints import EndpointResolverParams, Endpoint, resolve_static_uri
7+
from ..exceptions import EndpointResolutionError
8+
from ..interfaces import Endpoint as _Endpoint
9+
10+
11+
class StaticEndpointResolver(EndpointResolver):
12+
"""A basic endpoint resolver that forwards a static URI."""
13+
14+
async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> _Endpoint:
15+
static_uri = resolve_static_uri(params)
16+
if static_uri is None:
17+
raise EndpointResolutionError(
18+
"Unable to resolve endpoint: endpoint_uri is required"
19+
)
20+
21+
return Endpoint(uri=static_uri)

packages/smithy-core/src/smithy_core/aio/interfaces/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
from collections.abc import AsyncIterable
4-
from typing import Protocol, runtime_checkable, TYPE_CHECKING, Callable
4+
from typing import Protocol, runtime_checkable, TYPE_CHECKING, Callable, Any
55

66
from ...exceptions import UnsupportedStreamException
77
from ...interfaces import URI, Endpoint, TypedProperties
88
from ...interfaces import StreamingBlob as SyncStreamingBlob
99
from ...documents import TypeRegistry
10+
from ...endpoints import EndpointResolverParams
1011

1112
from .eventstream import EventPublisher, EventReceiver
1213

@@ -72,6 +73,17 @@ def consume_body(self) -> bytes:
7273
...
7374

7475

76+
class EndpointResolver(Protocol):
77+
"""Resolves an operation's endpoint based given parameters."""
78+
79+
async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoint:
80+
"""Resolve an endpoint for the given operation.
81+
82+
:param params: The parameters available to resolve the endpoint.
83+
"""
84+
...
85+
86+
7587
class ClientTransport[I: Request, O: Response](Protocol):
7688
"""Protocol-agnostic representation of a client tranport (e.g. an HTTP client)."""
7789

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Any
4+
from dataclasses import dataclass, field
5+
from urllib.parse import urlparse
6+
7+
from . import URI
8+
from .serializers import SerializeableShape
9+
from .schemas import APIOperation
10+
from .interfaces import TypedProperties as _TypedProperties
11+
from .interfaces import Endpoint as _Endpoint
12+
from .interfaces import URI as _URI
13+
from .types import TypedProperties, PropertyKey
14+
from .exceptions import EndpointResolutionError
15+
16+
17+
STATIC_URI: PropertyKey[str | _URI] = PropertyKey(
18+
key="endpoint_uri",
19+
# Python currently has problems expressing parametric types that can be
20+
# unions, literals, or other special types in addition to a class. So
21+
# we have to ignore the type below. PEP 747 should resolve the issue.
22+
# TODO: update this when PEP 747 lands
23+
value_type=str | _URI, # type: ignore
24+
)
25+
"""The property key for a statically defined URI."""
26+
27+
28+
@dataclass(kw_only=True)
29+
class Endpoint(_Endpoint):
30+
"""A resolved endpoint."""
31+
32+
uri: _URI
33+
"""The endpoint URI."""
34+
35+
properties: _TypedProperties = field(default_factory=TypedProperties)
36+
"""Properties required to interact with the endpoint.
37+
38+
For example, in some AWS use cases this might contain HTTP headers to add to each
39+
request.
40+
"""
41+
42+
43+
@dataclass(kw_only=True)
44+
class EndpointResolverParams[I: SerializeableShape]:
45+
"""Parameters passed into an Endpoint Resolver's resolve_endpoint method."""
46+
47+
operation: APIOperation[I, Any]
48+
"""The operation to resolve an endpoint for."""
49+
50+
input: I
51+
"""The input to the operation."""
52+
53+
context: _TypedProperties
54+
"""The context of the operation invocation."""
55+
56+
57+
def resolve_static_uri(
58+
properties: _TypedProperties | EndpointResolverParams[Any],
59+
) -> _URI | None:
60+
"""Attempt to resolve a static URI from the endpoint resolver params.
61+
62+
:param properties: A TypedProperties bag or EndpointResolverParams to search.
63+
"""
64+
properties = (
65+
properties.context
66+
if isinstance(properties, EndpointResolverParams)
67+
else properties
68+
)
69+
static_uri = properties.get(STATIC_URI)
70+
if static_uri is None:
71+
return None
72+
73+
# If it's not a string, it's already a parsed URI so just pass it along.
74+
if not isinstance(static_uri, str):
75+
return static_uri
76+
77+
parsed = urlparse(static_uri)
78+
if parsed.hostname is None:
79+
raise EndpointResolutionError(
80+
f"Unable to parse hostname from provided URI: {static_uri}"
81+
)
82+
83+
return URI(
84+
host=parsed.hostname,
85+
path=parsed.path,
86+
scheme=parsed.scheme,
87+
query=parsed.query,
88+
port=parsed.port,
89+
)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ class AsyncBodyException(SmithyException):
3333
class UnsupportedStreamException(SmithyException):
3434
"""Indicates that a serializer or deserializer's stream method was called, but data
3535
streams are not supported."""
36+
37+
38+
class EndpointResolutionError(SmithyException):
39+
"""Exception type for all exceptions raised by endpoint resolution."""

packages/smithy-core/src/smithy_core/interfaces/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,13 @@ def is_streaming_blob(obj: Any) -> TypeGuard[StreamingBlob]:
9595
return isinstance(obj, bytes | bytearray) or is_bytes_reader(obj)
9696

9797

98-
# TODO: update HTTP package and existing endpoint implementations to use this.
9998
class Endpoint(Protocol):
10099
"""A resolved endpoint."""
101100

102101
uri: URI
103102
"""The endpoint URI."""
104103

105-
# TODO: replace this with a typed context bag
106-
properties: dict[str, Any]
104+
properties: "TypedProperties"
107105
"""Properties required to interact with the endpoint.
108106
109107
For example, in some AWS use cases this might contain HTTP headers to add to each
@@ -138,6 +136,7 @@ class PropertyKey[T](Protocol):
138136
key: str
139137
"""The string key used to access the value."""
140138

139+
# TODO: update this when PEP 747 lands to allow for unions and literals
141140
value_type: type[T]
142141
"""The type of the associated value in the properties bag."""
143142

0 commit comments

Comments
 (0)