smithy-http 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import platform
4
+ from typing import Any, Self
5
+
6
+ import smithy_core
7
+ from smithy_core.interceptors import InputContext, Interceptor, RequestContext
8
+ from smithy_core.types import PropertyKey
9
+
10
+ from smithy_http import Field
11
+ from smithy_http.aio.interfaces import HTTPRequest
12
+ from smithy_http.user_agent import UserAgent, UserAgentComponent
13
+
14
+ USER_AGENT = PropertyKey(key="user_agent", value_type=UserAgent)
15
+
16
+
17
+ class UserAgentInterceptor(Interceptor[Any, Any, HTTPRequest, None]):
18
+ """Adds interceptors that initialize UserAgent in the context and add the user-agent
19
+ header."""
20
+
21
+ def read_before_execution(self, context: InputContext[Any]) -> None:
22
+ context.properties[USER_AGENT] = _UserAgentBuilder.from_environment().build()
23
+
24
+ def modify_before_signing(
25
+ self, context: RequestContext[Any, HTTPRequest]
26
+ ) -> HTTPRequest:
27
+ user_agent = context.properties[USER_AGENT]
28
+ request = context.transport_request
29
+ request.fields.set_field(Field(name="User-Agent", values=[str(user_agent)]))
30
+ return context.transport_request
31
+
32
+
33
+ _USERAGENT_ALLOWED_OS_NAMES = (
34
+ "windows",
35
+ "linux",
36
+ "macos",
37
+ "android",
38
+ "ios",
39
+ "watchos",
40
+ "tvos",
41
+ "other",
42
+ )
43
+ _USERAGENT_PLATFORM_NAME_MAPPINGS = {"darwin": "macos"}
44
+ _USERAGENT_SDK_NAME = "python"
45
+
46
+
47
+ class _UserAgentBuilder:
48
+ def __init__(
49
+ self,
50
+ *,
51
+ platform_name: str | None,
52
+ platform_version: str | None,
53
+ platform_machine: str | None,
54
+ python_version: str | None,
55
+ python_implementation: str | None,
56
+ sdk_version: str | None,
57
+ ) -> None:
58
+ self._platform_name = platform_name
59
+ self._platform_version = platform_version
60
+ self._platform_machine = platform_machine
61
+ self._python_version = python_version
62
+ self._python_implementation = python_implementation
63
+ self._sdk_version = sdk_version
64
+
65
+ @classmethod
66
+ def from_environment(cls) -> Self:
67
+ return cls(
68
+ platform_name=platform.system(),
69
+ platform_version=platform.release(),
70
+ platform_machine=platform.machine(),
71
+ python_version=platform.python_version(),
72
+ python_implementation=platform.python_implementation(),
73
+ sdk_version=smithy_core.__version__,
74
+ )
75
+
76
+ def build(self) -> UserAgent:
77
+ user_agent = UserAgent()
78
+ user_agent.sdk_metadata.extend(self._build_sdk_metadata())
79
+ user_agent.ua_metadata.append(UserAgentComponent(prefix="ua", name="2.1"))
80
+ user_agent.os_metadata.extend(self._build_os_metadata())
81
+ user_agent.os_metadata.extend(self._build_architecture_metadata())
82
+ user_agent.language_metadata.extend(self._build_language_metadata())
83
+
84
+ return user_agent
85
+
86
+ def _build_sdk_metadata(self) -> list[UserAgentComponent]:
87
+ if self._sdk_version:
88
+ return [
89
+ UserAgentComponent(prefix=_USERAGENT_SDK_NAME, name=self._sdk_version)
90
+ ]
91
+ return []
92
+
93
+ def _build_os_metadata(self) -> list[UserAgentComponent]:
94
+ """Build the OS/platform components of the User-Agent header string.
95
+
96
+ For recognized platform names that match or map to an entry in the list
97
+ of standardized OS names, a single component with prefix "os" is
98
+ returned. Otherwise, one component "os/other" is returned and a second
99
+ with prefix "md" and the raw platform name.
100
+
101
+ String representations of example return values:
102
+ * ``os/macos#10.13.6``
103
+ * ``os/linux``
104
+ * ``os/other``
105
+ * ``os/other md/foobar#1.2.3``
106
+ """
107
+ if self._platform_name in (None, ""):
108
+ return [UserAgentComponent("os", "other")]
109
+
110
+ plt_name_lower = self._platform_name.lower()
111
+ if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES:
112
+ os_family = plt_name_lower
113
+ elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS:
114
+ os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower]
115
+ else:
116
+ os_family = None
117
+
118
+ if os_family is not None:
119
+ return [UserAgentComponent("os", os_family, self._platform_version)]
120
+ else:
121
+ return [
122
+ UserAgentComponent("os", "other"),
123
+ UserAgentComponent("md", self._platform_name, self._platform_version),
124
+ ]
125
+
126
+ def _build_architecture_metadata(self) -> list[UserAgentComponent]:
127
+ """Build architecture component of the User-Agent header string.
128
+
129
+ Returns the machine type with prefix "md" and name "arch", if one is available.
130
+ Common values include "x86_64", "arm64", "i386".
131
+ """
132
+ if self._platform_machine:
133
+ return [UserAgentComponent("md", "arch", self._platform_machine.lower())]
134
+ return []
135
+
136
+ def _build_language_metadata(self) -> list[UserAgentComponent]:
137
+ """Build the language components of the User-Agent header string.
138
+
139
+ Returns the Python version in a component with prefix "lang" and name
140
+ "python". The Python implementation (e.g. CPython, PyPy) is returned as
141
+ separate metadata component with prefix "md" and name "pyimpl".
142
+
143
+ String representation of an example return value:
144
+ ``lang/python#3.10.4 md/pyimpl#CPython``
145
+ """
146
+ lang_md = [
147
+ UserAgentComponent("lang", "python", self._python_version),
148
+ ]
149
+ if self._python_implementation:
150
+ lang_md.append(
151
+ UserAgentComponent("md", "pyimpl", self._python_implementation)
152
+ )
153
+ return lang_md
@@ -0,0 +1,141 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from collections.abc import Iterator
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import Protocol
7
+
8
+
9
+ class FieldPosition(Enum):
10
+ """The type of a field.
11
+
12
+ Defines its placement in a request or response.
13
+ """
14
+
15
+ HEADER = 0
16
+ """Header field.
17
+
18
+ In HTTP this is a header as defined in RFC 9110 Section 6.3. Implementations of
19
+ other protocols may use this FieldPosition for similar types of metadata.
20
+ """
21
+
22
+ TRAILER = 1
23
+ """Trailer field.
24
+
25
+ In HTTP this is a trailer as defined in RFC 9110 Section 6.5. Implementations of
26
+ other protocols may use this FieldPosition for similar types of metadata.
27
+ """
28
+
29
+
30
+ class Field(Protocol):
31
+ """A name-value pair representing a single field in a request or response.
32
+
33
+ The kind will dictate metadata placement within an the message, for example as
34
+ header or trailer field in a HTTP request as defined in RFC 9110 Section 5.
35
+
36
+ All field names are case insensitive and case-variance must be treated as
37
+ equivalent. Names may be normalized but should be preserved for accuracy during
38
+ transmission.
39
+ """
40
+
41
+ name: str
42
+ values: list[str]
43
+ kind: FieldPosition = FieldPosition.HEADER
44
+
45
+ def add(self, value: str) -> None:
46
+ """Append a value to a field."""
47
+ ...
48
+
49
+ def set(self, values: list[str]) -> None:
50
+ """Overwrite existing field values."""
51
+ ...
52
+
53
+ def remove(self, value: str) -> None:
54
+ """Remove all matching entries from list."""
55
+ ...
56
+
57
+ def as_string(self) -> str:
58
+ """Serialize the ``Field``'s values into a single line string."""
59
+ ...
60
+
61
+ def as_tuples(self) -> list[tuple[str, str]]:
62
+ """Get list of ``name``, ``value`` tuples where each tuple represents one
63
+ value."""
64
+ ...
65
+
66
+
67
+ class Fields(Protocol):
68
+ """Protocol agnostic mapping of key-value pair request metadata, such as HTTP
69
+ fields."""
70
+
71
+ # Entries are keyed off the name of a provided Field
72
+ entries: dict[str, Field]
73
+ encoding: str = "utf-8"
74
+
75
+ def set_field(self, field: Field) -> None:
76
+ """Alias for __setitem__ to utilize the field.name for the entry key."""
77
+ ...
78
+
79
+ def __setitem__(self, name: str, field: Field) -> None:
80
+ """Set entry for a Field name."""
81
+ ...
82
+
83
+ def __getitem__(self, name: str) -> Field:
84
+ """Retrieve Field entry."""
85
+ ...
86
+
87
+ def __delitem__(self, name: str) -> None:
88
+ """Delete entry from collection."""
89
+ ...
90
+
91
+ def __iter__(self) -> Iterator[Field]:
92
+ """Allow iteration over entries."""
93
+ ...
94
+
95
+ def __len__(self) -> int:
96
+ """Get total number of Field entries."""
97
+ ...
98
+
99
+ def __contains__(self, key: str) -> bool:
100
+ """Allows in/not in checks on Field entries."""
101
+ ...
102
+
103
+ def get_by_type(self, kind: FieldPosition) -> list[Field]:
104
+ """Helper function for retrieving specific types of fields.
105
+
106
+ Used to grab all headers or all trailers.
107
+ """
108
+ ...
109
+
110
+ def extend(self, other: "Fields") -> None:
111
+ """Merges ``entries`` of ``other`` into the current ``entries``.
112
+
113
+ For every `Field` in the ``entries`` of ``other``: If the normalized name
114
+ already exists in the current ``entries``, the values from ``other`` are
115
+ appended. Otherwise, the ``Field`` is added to the list of ``entries``.
116
+ """
117
+ ...
118
+
119
+
120
+ QueryParamsList = list[tuple[str, str]]
121
+
122
+
123
+ @dataclass(kw_only=True)
124
+ class HTTPClientConfiguration:
125
+ """Client-level HTTP configuration.
126
+
127
+ :param force_http_2: Whether to require HTTP/2.
128
+ """
129
+
130
+ force_http_2: bool = False
131
+
132
+
133
+ @dataclass(kw_only=True)
134
+ class HTTPRequestConfiguration:
135
+ """Request-level HTTP configuration.
136
+
137
+ :param read_timeout: How long, in seconds, the client will attempt to read the first
138
+ byte over an established, open connection before timing out.
139
+ """
140
+
141
+ read_timeout: float | None = None
@@ -0,0 +1,15 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from typing import Any, Protocol
4
+
5
+ from smithy_core.interceptors import Interceptor
6
+
7
+ from smithy_http.interceptors.user_agent import UserAgentInterceptor
8
+
9
+
10
+ class _InterceptorConfig(Protocol):
11
+ interceptors: list[Interceptor[Any, Any, Any, Any]]
12
+
13
+
14
+ def user_agent_plugin(config: _InterceptorConfig) -> None:
15
+ config.interceptors.append(UserAgentInterceptor())
smithy_http/py.typed ADDED
File without changes
@@ -0,0 +1,29 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from typing import NamedTuple
4
+
5
+ from smithy_core.documents import DocumentValue
6
+
7
+ _REST_JSON_CODE_HEADER = "x-amzn-errortype"
8
+
9
+ _REST_JSON_CODE_KEYS = {"__type", "code"}
10
+
11
+ _REST_JSON_MESSAGE_KEYS = {"message", "errormessage", "error_message"}
12
+
13
+
14
+ class RestJsonErrorInfo(NamedTuple):
15
+ """Generic error information from a RestJson protocol error."""
16
+
17
+ code: str
18
+ """The error code."""
19
+
20
+ message: str
21
+ """The generic error message.
22
+
23
+ A modeled error may have the error bound somewhere else. This is based off of
24
+ checking the most common locations and is intended for use with excpetions that
25
+ either didn't model the message or which are unknown.
26
+ """
27
+
28
+ json_body: dict[str, DocumentValue] | None = None
29
+ """The HTTP response body parsed as JSON."""