1414import os
1515import platform
1616from string import ascii_letters , digits
17- from typing import NamedTuple , Optional , Self
17+ from typing import NamedTuple , Optional , Self , Union , List
1818
1919from smithy_http .aio .crt import HAS_CRT
2020
3333_USERAGENT_SDK_NAME = "aws-sdk-python"
3434
3535
36+ class UserAgentComponent (NamedTuple ):
37+ """Component of a User-Agent header string in the standard format.
38+
39+ Each component consists of a prefix, a name, and a value. In the string
40+ representation these are combined in the format ``prefix/name#value``.
41+
42+ This class is considered private and is subject to abrupt breaking changes.
43+ """
44+
45+ prefix : str
46+ name : str
47+ value : Optional [str ] = None
48+
49+ def to_string (self ):
50+ """Create string like 'prefix/name#value' from a UserAgentComponent."""
51+ clean_prefix = sanitize_user_agent_string_component (
52+ self .prefix , allow_hash = True
53+ )
54+ clean_name = sanitize_user_agent_string_component (self .name , allow_hash = False )
55+ if self .value is None or self .value == "" :
56+ return f"{ clean_prefix } /{ clean_name } "
57+ clean_value = sanitize_user_agent_string_component (self .value , allow_hash = True )
58+ return f"{ clean_prefix } /{ clean_name } #{ clean_value } "
59+
60+
61+ class RawStringUserAgentComponent :
62+ """UserAgentComponent interface wrapper around ``str``.
63+
64+ Use for User-Agent header components that are not constructed from prefix+name+value
65+ but instead are provided as strings. No sanitization is performed.
66+ """
67+
68+ def __init__ (self , value : str ):
69+ self ._value = value
70+
71+ def to_string (self ) -> str :
72+ return self ._value
73+
74+
75+ _UAComponent = Union [UserAgentComponent , RawStringUserAgentComponent ]
76+
77+
3678class UserAgent :
3779 def __init__ (
3880 self ,
39- platform_name ,
40- platform_version ,
41- platform_machine ,
42- python_version ,
43- python_implementation ,
44- execution_env ,
45- crt_version = None ,
81+ platform_name : str | None ,
82+ platform_version : str | None ,
83+ platform_machine : str | None ,
84+ python_version : str | None ,
85+ python_implementation : str | None ,
86+ execution_env : str | None ,
87+ crt_version : str | None ,
4688 ) -> None :
4789 self ._platform_name = platform_name
4890 self ._platform_version = platform_version
@@ -74,16 +116,16 @@ def from_environment(cls) -> Self:
74116
75117 def with_config (
76118 self ,
77- ua_suffix : str | None = None ,
78- ua_app_id : str | None = None ,
79- sdk_version : str | None = None ,
119+ ua_suffix : str | None ,
120+ ua_app_id : str | None ,
121+ sdk_version : str | None ,
80122 ) -> Self :
81123 self ._user_agent_suffix = ua_suffix
82124 self ._user_agent_app_id = ua_app_id
83125 self ._sdk_version = sdk_version
84126 return self
85127
86- def to_string (self ):
128+ def to_string (self ) -> str :
87129 """Build User-Agent header string from the object's properties."""
88130 components = [
89131 * self ._build_sdk_metadata (),
@@ -99,20 +141,22 @@ def to_string(self):
99141
100142 return " " .join ([comp .to_string () for comp in components ])
101143
102- def _build_sdk_metadata (self ):
144+ def _build_sdk_metadata (self ) -> List [ UserAgentComponent ] :
103145 """Build the SDK name and version component of the User-Agent header.
104146
105147 Includes CRT version if available.
106148 """
107- sdk_md = []
108- sdk_md .append (UserAgentComponent (_USERAGENT_SDK_NAME , self ._sdk_version ))
149+ sdk_version = self ._sdk_version if self ._sdk_version else "Unknown"
150+ sdk_md : List [UserAgentComponent ] = [
151+ UserAgentComponent (_USERAGENT_SDK_NAME , sdk_version )
152+ ]
109153
110154 if self ._crt_version is not None :
111155 sdk_md .append (UserAgentComponent ("md" , "awscrt" , self ._crt_version ))
112156
113157 return sdk_md
114158
115- def _build_os_metadata (self ):
159+ def _build_os_metadata (self ) -> List [ UserAgentComponent ] :
116160 """Build the OS/platform components of the User-Agent header string.
117161
118162 For recognized platform names that match or map to an entry in the list
@@ -145,7 +189,7 @@ def _build_os_metadata(self):
145189 UserAgentComponent ("md" , self ._platform_name , self ._platform_version ),
146190 ]
147191
148- def _build_architecture_metadata (self ):
192+ def _build_architecture_metadata (self ) -> List [ UserAgentComponent ] :
149193 """Build architecture component of the User-Agent header string.
150194
151195 Returns the machine type with prefix "md" and name "arch", if one is available.
@@ -155,7 +199,7 @@ def _build_architecture_metadata(self):
155199 return [UserAgentComponent ("md" , "arch" , self ._platform_machine .lower ())]
156200 return []
157201
158- def _build_language_metadata (self ):
202+ def _build_language_metadata (self ) -> List [ UserAgentComponent ] :
159203 """Build the language components of the User-Agent header string.
160204
161205 Returns the Python version in a component with prefix "lang" and name
@@ -174,7 +218,7 @@ def _build_language_metadata(self):
174218 )
175219 return lang_md
176220
177- def _build_execution_env_metadata (self ):
221+ def _build_execution_env_metadata (self ) -> List [ UserAgentComponent ] :
178222 """Build the execution environment component of the User-Agent header.
179223
180224 Returns a single component prefixed with "exec-env", usually sourced from the
@@ -185,28 +229,28 @@ def _build_execution_env_metadata(self):
185229 else :
186230 return []
187231
188- def _build_feature_metadata (self ):
232+ def _build_feature_metadata (self ) -> List [ UserAgentComponent ] :
189233 """Build the features components of the User-Agent header string.
190234
191235 TODO: These should be sourced from property bag set on context.
192236 """
193237 return []
194238
195- def _build_app_id (self ):
239+ def _build_app_id (self ) -> List [ UserAgentComponent ] :
196240 """Build app component of the User-Agent header string."""
197241 if self ._user_agent_app_id :
198242 return [UserAgentComponent ("app" , self ._user_agent_app_id )]
199243 else :
200244 return []
201245
202- def _build_suffix (self ):
246+ def _build_suffix (self ) -> List [ _UAComponent ] :
203247 if self ._user_agent_suffix :
204248 return [RawStringUserAgentComponent (self ._user_agent_suffix )]
205249 else :
206250 return []
207251
208252
209- def sanitize_user_agent_string_component (raw_str , allow_hash ) :
253+ def sanitize_user_agent_string_component (raw_str : str , allow_hash : bool = False ) -> str :
210254 """Replaces all not allowed characters in the string with a dash ("-").
211255
212256 Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If
@@ -224,46 +268,7 @@ def sanitize_user_agent_string_component(raw_str, allow_hash):
224268 )
225269
226270
227- class UserAgentComponent (NamedTuple ):
228- """Component of a User-Agent header string in the standard format.
229-
230- Each component consists of a prefix, a name, and a value. In the string
231- representation these are combined in the format ``prefix/name#value``.
232-
233- This class is considered private and is subject to abrupt breaking changes.
234- """
235-
236- prefix : str
237- name : str
238- value : Optional [str ] = None
239-
240- def to_string (self ):
241- """Create string like 'prefix/name#value' from a UserAgentComponent."""
242- clean_prefix = sanitize_user_agent_string_component (
243- self .prefix , allow_hash = True
244- )
245- clean_name = sanitize_user_agent_string_component (self .name , allow_hash = False )
246- if self .value is None or self .value == "" :
247- return f"{ clean_prefix } /{ clean_name } "
248- clean_value = sanitize_user_agent_string_component (self .value , allow_hash = True )
249- return f"{ clean_prefix } /{ clean_name } #{ clean_value } "
250-
251-
252- class RawStringUserAgentComponent :
253- """UserAgentComponent interface wrapper around ``str``.
254-
255- Use for User-Agent header components that are not constructed from prefix+name+value
256- but instead are provided as strings. No sanitization is performed.
257- """
258-
259- def __init__ (self , value ):
260- self ._value = value
261-
262- def to_string (self ):
263- return self ._value
264-
265-
266- def _get_crt_version ():
271+ def _get_crt_version () -> str | None :
267272 """This function is considered private and is subject to abrupt breaking changes."""
268273 try :
269274 import awscrt
0 commit comments