okta-client-python 0.1.0__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.
- okta_client/__init__.py +20 -0
- okta_client/authfoundation/__init__.py +197 -0
- okta_client/authfoundation/authentication.py +247 -0
- okta_client/authfoundation/coalesced_result.py +113 -0
- okta_client/authfoundation/codable.py +72 -0
- okta_client/authfoundation/expires.py +49 -0
- okta_client/authfoundation/key_provider.py +130 -0
- okta_client/authfoundation/networking/__init__.py +56 -0
- okta_client/authfoundation/networking/body.py +46 -0
- okta_client/authfoundation/networking/client.py +200 -0
- okta_client/authfoundation/networking/types.py +293 -0
- okta_client/authfoundation/oauth2/__init__.py +104 -0
- okta_client/authfoundation/oauth2/claims.py +44 -0
- okta_client/authfoundation/oauth2/client.py +402 -0
- okta_client/authfoundation/oauth2/client_authorization.py +172 -0
- okta_client/authfoundation/oauth2/config.py +298 -0
- okta_client/authfoundation/oauth2/errors.py +32 -0
- okta_client/authfoundation/oauth2/jwt_bearer_claims.py +59 -0
- okta_client/authfoundation/oauth2/jwt_bearer_utils.py +30 -0
- okta_client/authfoundation/oauth2/jwt_context.py +52 -0
- okta_client/authfoundation/oauth2/jwt_token.py +214 -0
- okta_client/authfoundation/oauth2/models.py +198 -0
- okta_client/authfoundation/oauth2/parameters.py +36 -0
- okta_client/authfoundation/oauth2/refresh_token.py +165 -0
- okta_client/authfoundation/oauth2/request_protocols.py +174 -0
- okta_client/authfoundation/oauth2/requests/__init__.py +37 -0
- okta_client/authfoundation/oauth2/requests/introspect.py +50 -0
- okta_client/authfoundation/oauth2/requests/jwks.py +44 -0
- okta_client/authfoundation/oauth2/requests/oauth_authorization_server.py +44 -0
- okta_client/authfoundation/oauth2/requests/openid_configuration.py +47 -0
- okta_client/authfoundation/oauth2/requests/revoke.py +54 -0
- okta_client/authfoundation/oauth2/requests/user_info.py +37 -0
- okta_client/authfoundation/oauth2/utils.py +25 -0
- okta_client/authfoundation/oauth2/validation_protocols.py +33 -0
- okta_client/authfoundation/oauth2/validator_registry.py +64 -0
- okta_client/authfoundation/oauth2/validators/token_hash.py +37 -0
- okta_client/authfoundation/oauth2/validators/token_validator.py +26 -0
- okta_client/authfoundation/time_coordinator.py +57 -0
- okta_client/authfoundation/token.py +201 -0
- okta_client/authfoundation/user_agent.py +80 -0
- okta_client/authfoundation/utils.py +63 -0
- okta_client/browser_signin/__init__.py +11 -0
- okta_client/directauth/__init__.py +11 -0
- okta_client/oauth2auth/__init__.py +63 -0
- okta_client/oauth2auth/authorization_code.py +594 -0
- okta_client/oauth2auth/cross_app.py +626 -0
- okta_client/oauth2auth/jwt_bearer.py +182 -0
- okta_client/oauth2auth/resource_owner.py +159 -0
- okta_client/oauth2auth/token_exchange.py +380 -0
- okta_client/oauth2auth/utils.py +87 -0
- okta_client/py.typed +0 -0
- okta_client_python-0.1.0.dist-info/METADATA +936 -0
- okta_client_python-0.1.0.dist-info/RECORD +56 -0
- okta_client_python-0.1.0.dist-info/WHEEL +5 -0
- okta_client_python-0.1.0.dist-info/licenses/LICENSE +171 -0
- okta_client_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import threading
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
import jwt
|
|
20
|
+
from jwt import algorithms as jwt_algorithms
|
|
21
|
+
|
|
22
|
+
_ALLOWED_ALGORITHMS = {
|
|
23
|
+
"HS256",
|
|
24
|
+
"HS384",
|
|
25
|
+
"HS512",
|
|
26
|
+
"RS256",
|
|
27
|
+
"RS384",
|
|
28
|
+
"RS512",
|
|
29
|
+
"ES256",
|
|
30
|
+
"ES384",
|
|
31
|
+
"ES512",
|
|
32
|
+
"PS256",
|
|
33
|
+
"PS384",
|
|
34
|
+
"PS512",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class KeyProvider(Protocol):
|
|
40
|
+
"""Protocol for signing JWT assertions."""
|
|
41
|
+
|
|
42
|
+
algorithm: str
|
|
43
|
+
key_id: str | None
|
|
44
|
+
|
|
45
|
+
def sign_jwt(self, claims: Mapping[str, object], headers: Mapping[str, object] | None = None) -> str:
|
|
46
|
+
"""Sign the given claims and return a compact JWT string."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class LocalKeyProvider(KeyProvider):
|
|
52
|
+
"""Key provider that signs JWTs using local key material."""
|
|
53
|
+
|
|
54
|
+
key: str | bytes | Mapping[str, Any]
|
|
55
|
+
algorithm: str = "RS256"
|
|
56
|
+
key_id: str | None = None
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if self.algorithm not in _ALLOWED_ALGORITHMS:
|
|
60
|
+
raise ValueError(f"Unsupported JWT algorithm: {self.algorithm}")
|
|
61
|
+
|
|
62
|
+
def sign_jwt(self, claims: Mapping[str, object], headers: Mapping[str, object] | None = None) -> str:
|
|
63
|
+
if not claims:
|
|
64
|
+
raise ValueError("claims must not be empty")
|
|
65
|
+
encoded_headers = dict(headers or {})
|
|
66
|
+
if self.key_id and "kid" not in encoded_headers:
|
|
67
|
+
encoded_headers["kid"] = self.key_id
|
|
68
|
+
key = _resolve_key_material(self.key, self.algorithm)
|
|
69
|
+
token = jwt.encode(payload=dict(claims), key=key, algorithm=self.algorithm, headers=encoded_headers or None)
|
|
70
|
+
return token.decode("utf-8") if isinstance(token, bytes) else token
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_pem(
|
|
74
|
+
cls,
|
|
75
|
+
pem: str,
|
|
76
|
+
*,
|
|
77
|
+
algorithm: str = "RS256",
|
|
78
|
+
key_id: str | None = None,
|
|
79
|
+
) -> LocalKeyProvider:
|
|
80
|
+
return cls(pem, algorithm=algorithm, key_id=key_id)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_pem_file(
|
|
84
|
+
cls,
|
|
85
|
+
path: str,
|
|
86
|
+
*,
|
|
87
|
+
algorithm: str = "RS256",
|
|
88
|
+
key_id: str | None = None,
|
|
89
|
+
encoding: str = "utf-8",
|
|
90
|
+
) -> LocalKeyProvider:
|
|
91
|
+
with open(path, encoding=encoding) as handle:
|
|
92
|
+
return cls(handle.read(), algorithm=algorithm, key_id=key_id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DefaultKeyProvider(KeyProvider):
|
|
96
|
+
"""Default provider that requires explicit configuration."""
|
|
97
|
+
|
|
98
|
+
algorithm: str = ""
|
|
99
|
+
key_id: str | None = None
|
|
100
|
+
|
|
101
|
+
def sign_jwt(self, claims: Mapping[str, object], headers: Mapping[str, object] | None = None) -> str:
|
|
102
|
+
raise RuntimeError("KeyProvider is not configured. Use set_key_provider() to supply a signer.")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_default_key_provider: KeyProvider = DefaultKeyProvider()
|
|
106
|
+
_key_provider_lock = threading.Lock()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_key_provider() -> KeyProvider:
|
|
110
|
+
"""Get the global KeyProvider in a thread-safe manner."""
|
|
111
|
+
with _key_provider_lock:
|
|
112
|
+
return _default_key_provider
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def set_key_provider(provider: KeyProvider) -> None:
|
|
116
|
+
"""Set the global KeyProvider in a thread-safe manner."""
|
|
117
|
+
global _default_key_provider
|
|
118
|
+
with _key_provider_lock:
|
|
119
|
+
_default_key_provider = provider
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _resolve_key_material(key: str | bytes | Mapping[str, Any], algorithm: str) -> Any:
|
|
123
|
+
if isinstance(key, (str, bytes)):
|
|
124
|
+
return key
|
|
125
|
+
if isinstance(key, Mapping):
|
|
126
|
+
algorithms = jwt_algorithms.get_default_algorithms()
|
|
127
|
+
if algorithm not in algorithms:
|
|
128
|
+
raise ValueError(f"Unsupported JWT algorithm: {algorithm}")
|
|
129
|
+
return algorithms[algorithm].from_jwk(json.dumps(dict(key)))
|
|
130
|
+
raise TypeError("key must be a PEM string, bytes, or JWK mapping")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from .body import APIRequestBodyMixin
|
|
12
|
+
from .client import APIClient, DefaultNetworkInterface
|
|
13
|
+
from .types import (
|
|
14
|
+
APIAuthorization,
|
|
15
|
+
APIClientConfiguration,
|
|
16
|
+
APIClientListener,
|
|
17
|
+
APIContentType,
|
|
18
|
+
APIParsingContext,
|
|
19
|
+
APIRateLimit,
|
|
20
|
+
APIRequest,
|
|
21
|
+
APIRequestBody,
|
|
22
|
+
APIRequestMethod,
|
|
23
|
+
APIResponse,
|
|
24
|
+
APIRetry,
|
|
25
|
+
BaseAPIRequest,
|
|
26
|
+
HTTPRequest,
|
|
27
|
+
ListenerCollection,
|
|
28
|
+
NetworkInterface,
|
|
29
|
+
RawResponse,
|
|
30
|
+
RequestValue,
|
|
31
|
+
RequestValueConvertible,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"APIAuthorization",
|
|
36
|
+
"APIClient",
|
|
37
|
+
"APIClientConfiguration",
|
|
38
|
+
"APIClientListener",
|
|
39
|
+
"APIContentType",
|
|
40
|
+
"APIParsingContext",
|
|
41
|
+
"APIRateLimit",
|
|
42
|
+
"APIRequest",
|
|
43
|
+
"APIRequestBody",
|
|
44
|
+
"APIRequestBodyMixin",
|
|
45
|
+
"APIRequestMethod",
|
|
46
|
+
"APIResponse",
|
|
47
|
+
"APIRetry",
|
|
48
|
+
"BaseAPIRequest",
|
|
49
|
+
"DefaultNetworkInterface",
|
|
50
|
+
"HTTPRequest",
|
|
51
|
+
"ListenerCollection",
|
|
52
|
+
"NetworkInterface",
|
|
53
|
+
"RawResponse",
|
|
54
|
+
"RequestValue",
|
|
55
|
+
"RequestValueConvertible",
|
|
56
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import parse_qs, urlencode
|
|
16
|
+
|
|
17
|
+
from okta_client.authfoundation.utils import serialize_parameters
|
|
18
|
+
|
|
19
|
+
from .types import APIContentType, APIParsingContext, APIRequestBody, RawResponse
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APIRequestBodyMixin(APIRequestBody):
|
|
23
|
+
"""Mixin that serializes body parameters based on content type."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def content_type(self) -> APIContentType | None:
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def accepts_type(self) -> APIContentType | None:
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def body(self) -> bytes | None:
|
|
34
|
+
params = serialize_parameters(self.body_parameters)
|
|
35
|
+
if self.content_type == APIContentType.JSON:
|
|
36
|
+
return json.dumps(params).encode("utf-8")
|
|
37
|
+
return urlencode(params).encode("utf-8")
|
|
38
|
+
|
|
39
|
+
def parse_response(self, response: RawResponse, parsing_context: APIParsingContext | None = None) -> Any:
|
|
40
|
+
if not response.body:
|
|
41
|
+
return {}
|
|
42
|
+
if self.accepts_type == APIContentType.JSON:
|
|
43
|
+
return json.loads(response.body.decode("utf-8"))
|
|
44
|
+
if self.accepts_type == APIContentType.FORM_URLENCODED:
|
|
45
|
+
return parse_qs(response.body.decode("utf-8"))
|
|
46
|
+
return response.body.decode("utf-8")
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# The Okta software accompanied by this notice is provided pursuant to the following terms:
|
|
2
|
+
# Copyright © 2026-Present, Okta, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
4
|
+
# License.
|
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
8
|
+
# See the License for the specific language governing permissions and limitations under the License.
|
|
9
|
+
# coding: utf-8
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from typing import Any, TypeVar
|
|
18
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
19
|
+
|
|
20
|
+
from okta_client.authfoundation.utils import serialize_parameters, serialize_request_value
|
|
21
|
+
|
|
22
|
+
from .types import (
|
|
23
|
+
APIClientConfiguration,
|
|
24
|
+
APIClientListener,
|
|
25
|
+
APIContentType,
|
|
26
|
+
APIParsingContext,
|
|
27
|
+
APIRateLimit,
|
|
28
|
+
APIRequest,
|
|
29
|
+
APIRequestBody,
|
|
30
|
+
APIResponse,
|
|
31
|
+
APIRetry,
|
|
32
|
+
HTTPRequest,
|
|
33
|
+
ListenerCollection,
|
|
34
|
+
NetworkInterface,
|
|
35
|
+
RawResponse,
|
|
36
|
+
RequestValue,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class APIClient:
|
|
43
|
+
"""Network client that executes APIRequest objects and parses responses."""
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
base_url: str | None = None,
|
|
47
|
+
user_agent: str | None = None,
|
|
48
|
+
additional_http_headers: Mapping[str, str] | None = None,
|
|
49
|
+
request_id_header: str | None = None,
|
|
50
|
+
configuration: APIClientConfiguration | None = None,
|
|
51
|
+
network: NetworkInterface | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Create a client with configuration and network transport."""
|
|
54
|
+
if configuration is None:
|
|
55
|
+
if base_url is None or user_agent is None:
|
|
56
|
+
raise ValueError("base_url and user_agent are required when configuration is not provided")
|
|
57
|
+
configuration = APIClientConfiguration(
|
|
58
|
+
base_url=base_url,
|
|
59
|
+
user_agent=user_agent,
|
|
60
|
+
additional_http_headers=additional_http_headers or {},
|
|
61
|
+
request_id_header=request_id_header,
|
|
62
|
+
)
|
|
63
|
+
self.configuration = configuration
|
|
64
|
+
self.base_url = configuration.base_url
|
|
65
|
+
self.user_agent = configuration.user_agent
|
|
66
|
+
self.network = network or DefaultNetworkInterface()
|
|
67
|
+
self.additional_http_headers = dict(configuration.additional_http_headers or {})
|
|
68
|
+
self.request_id_header = configuration.request_id_header
|
|
69
|
+
self._listeners: ListenerCollection[APIClientListener] = ListenerCollection()
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def listeners(self) -> ListenerCollection[APIClientListener]:
|
|
73
|
+
"""Registered listeners for request lifecycle events."""
|
|
74
|
+
return self._listeners
|
|
75
|
+
|
|
76
|
+
def send(self, request: APIRequest[T], parsing_context: APIParsingContext | None = None) -> APIResponse[T]:
|
|
77
|
+
"""Send an APIRequest and return a typed APIResponse."""
|
|
78
|
+
http_request = self.build_http_request(request)
|
|
79
|
+
self.will_send(http_request)
|
|
80
|
+
try:
|
|
81
|
+
raw_response = self._send_once(request, http_request=http_request)
|
|
82
|
+
result = request.parse_response(raw_response, parsing_context=parsing_context)
|
|
83
|
+
response = APIResponse(
|
|
84
|
+
result=result,
|
|
85
|
+
status_code=raw_response.status_code,
|
|
86
|
+
headers=raw_response.headers,
|
|
87
|
+
request_id=self._extract_request_id(raw_response.headers),
|
|
88
|
+
rate_limit=None,
|
|
89
|
+
links=None,
|
|
90
|
+
)
|
|
91
|
+
self.did_send(http_request, response)
|
|
92
|
+
return response
|
|
93
|
+
except Exception as error:
|
|
94
|
+
self.did_send_error(http_request, error)
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
def will_send(self, request: HTTPRequest) -> None:
|
|
98
|
+
"""Notify listeners before a request is sent."""
|
|
99
|
+
for listener in self._listeners:
|
|
100
|
+
listener.will_send(self, request)
|
|
101
|
+
|
|
102
|
+
def did_send(self, request: HTTPRequest, response: APIResponse[Any]) -> None:
|
|
103
|
+
"""Notify listeners after a successful response."""
|
|
104
|
+
for listener in self._listeners:
|
|
105
|
+
listener.did_send(self, request, response)
|
|
106
|
+
|
|
107
|
+
def did_send_error(self, request: HTTPRequest, error: Exception) -> None:
|
|
108
|
+
"""Notify listeners after a failed request."""
|
|
109
|
+
for listener in self._listeners:
|
|
110
|
+
listener.did_send_error(self, request, error)
|
|
111
|
+
|
|
112
|
+
def should_retry(self, request: HTTPRequest, rate_limit: APIRateLimit | None = None) -> APIRetry:
|
|
113
|
+
"""Ask listeners for a retry policy for a request."""
|
|
114
|
+
for listener in self._listeners:
|
|
115
|
+
retry = listener.should_retry(self, request, rate_limit)
|
|
116
|
+
if retry.kind != "default":
|
|
117
|
+
return retry
|
|
118
|
+
return APIRetry.default()
|
|
119
|
+
|
|
120
|
+
def _send_once(self, request: APIRequest[T], http_request: HTTPRequest | None = None) -> RawResponse:
|
|
121
|
+
"""Send a request once using the configured network interface."""
|
|
122
|
+
if not self.network:
|
|
123
|
+
raise RuntimeError("APIClient.network is not configured")
|
|
124
|
+
built_request = http_request or self.build_http_request(request)
|
|
125
|
+
return self.network.send(built_request)
|
|
126
|
+
|
|
127
|
+
def build_http_request(self, request: APIRequest[Any]) -> HTTPRequest:
|
|
128
|
+
"""Build a platform HTTP request from an APIRequest."""
|
|
129
|
+
headers = dict(self._build_headers(request))
|
|
130
|
+
url = self._build_url(request.url, request.query)
|
|
131
|
+
body = self._build_body(request)
|
|
132
|
+
timeout = request.timeout if request.timeout is not None else self.configuration.timeout
|
|
133
|
+
http_request = HTTPRequest(method=request.http_method, url=url, headers=headers, body=body, timeout=timeout)
|
|
134
|
+
if request.authorization:
|
|
135
|
+
http_request = request.authorization.authorize(http_request)
|
|
136
|
+
return http_request
|
|
137
|
+
|
|
138
|
+
def _build_headers(self, request: APIRequest[Any]) -> Mapping[str, str]:
|
|
139
|
+
"""Build request headers from defaults and request data."""
|
|
140
|
+
headers: dict[str, str] = {}
|
|
141
|
+
headers.update(self.additional_http_headers)
|
|
142
|
+
headers["User-Agent"] = self.user_agent
|
|
143
|
+
if request.headers:
|
|
144
|
+
for key, value in request.headers.items():
|
|
145
|
+
serialized = serialize_request_value(value)
|
|
146
|
+
if serialized is not None:
|
|
147
|
+
headers[key] = serialized
|
|
148
|
+
if request.accepts_type:
|
|
149
|
+
headers["Accept"] = request.accepts_type.value
|
|
150
|
+
if request.content_type:
|
|
151
|
+
headers["Content-Type"] = request.content_type.value
|
|
152
|
+
return headers
|
|
153
|
+
|
|
154
|
+
def _build_url(self, url: str, query: Mapping[str, RequestValue] | None) -> str:
|
|
155
|
+
"""Build a URL with merged query parameters."""
|
|
156
|
+
if not query:
|
|
157
|
+
return url
|
|
158
|
+
query_string = urlencode(serialize_parameters(query))
|
|
159
|
+
split = urlsplit(url)
|
|
160
|
+
merged_query = "&".join(filter(None, [split.query, query_string]))
|
|
161
|
+
return urlunsplit((split.scheme, split.netloc, split.path, merged_query, split.fragment))
|
|
162
|
+
|
|
163
|
+
def _build_body(self, request: APIRequest[Any]) -> bytes | None:
|
|
164
|
+
"""Serialize the request body based on content type."""
|
|
165
|
+
if isinstance(request, APIRequestBody):
|
|
166
|
+
params = serialize_parameters(request.body_parameters)
|
|
167
|
+
if request.content_type == APIContentType.JSON:
|
|
168
|
+
return json.dumps(params).encode("utf-8")
|
|
169
|
+
return urlencode(params).encode("utf-8")
|
|
170
|
+
return request.body()
|
|
171
|
+
|
|
172
|
+
def _extract_request_id(self, headers: Mapping[str, str]) -> str | None:
|
|
173
|
+
"""Extract a request ID from response headers, if configured."""
|
|
174
|
+
if not self.request_id_header:
|
|
175
|
+
return None
|
|
176
|
+
for key, value in headers.items():
|
|
177
|
+
if key.lower() == self.request_id_header.lower():
|
|
178
|
+
return value
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class DefaultNetworkInterface(NetworkInterface):
|
|
183
|
+
"""Default network interface using urllib.request."""
|
|
184
|
+
|
|
185
|
+
def send(self, request: HTTPRequest) -> RawResponse:
|
|
186
|
+
req = urllib.request.Request(
|
|
187
|
+
request.url,
|
|
188
|
+
data=request.body,
|
|
189
|
+
headers=request.headers,
|
|
190
|
+
method=request.method.value,
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
with urllib.request.urlopen(req, timeout=request.timeout) as response:
|
|
194
|
+
body = response.read() or b""
|
|
195
|
+
headers = {k: v for k, v in response.headers.items()}
|
|
196
|
+
return RawResponse(status_code=response.status, headers=headers, body=body)
|
|
197
|
+
except urllib.error.HTTPError as error:
|
|
198
|
+
body = error.read() or b""
|
|
199
|
+
headers = {k: v for k, v in error.headers.items()}
|
|
200
|
+
return RawResponse(status_code=error.code, headers=headers, body=body)
|