Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic support for client context params #3037

Merged
merged 6 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions botocore/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def compute_client_args(
disable_request_compression=(
client_config.disable_request_compression
),
client_context_params=client_config.client_context_params,
)
self._compute_retry_config(config_kwargs)
self._compute_connect_timeout(config_kwargs)
Expand Down Expand Up @@ -641,14 +642,16 @@ def _build_endpoint_resolver(
client_endpoint_url=endpoint_url,
legacy_endpoint_url=endpoint.host,
)
# botocore does not support client context parameters generically
# for every service. Instead, the s3 config section entries are
# available as client context parameters. In the future, endpoint
# rulesets of services other than s3/s3control may require client
# context parameters.
client_context = (
s3_config_raw if self._is_s3_service(service_name_raw) else {}
)
# Client context params for s3 conflict with the available settings
# in the `s3` parameter on the `Config` object. The s3 config will
# always take precedence over the client context params for s3 and
# s3control if set.
if self._is_s3_service(service_name_raw) and s3_config_raw:
client_context = s3_config_raw
elif client_config is not None and client_config.client_context_params:
client_context = client_config.client_context_params
else:
client_context = {}
sig_version = (
client_config.signature_version
if client_config is not None
Expand Down
11 changes: 11 additions & 0 deletions botocore/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ class Config:
set to True.

Defaults to None.

:type client_context_params: dict
:param client_context_params: A dictionary of parameters specific to
individual services. If available, valid parameters can be found in the
``Client Context Parameters`` section of the service's client homepage.
Invalid parameters or ones that are not used by the specified service
will be ignored.

Defaults to None.

"""

OPTION_DEFAULTS = OrderedDict(
Expand Down Expand Up @@ -247,6 +257,7 @@ class Config:
('tcp_keepalive', None),
('request_min_compression_size_bytes', None),
('disable_request_compression', None),
('client_context_params', None),
]
)

Expand Down
44 changes: 44 additions & 0 deletions botocore/docs/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# language governing permissions and limitations under the License.
import os

from botocore import xform_name
from botocore.compat import OrderedDict
from botocore.docs.bcdoc.restdoc import DocumentStructure
from botocore.docs.example import ResponseExampleDocumenter
Expand Down Expand Up @@ -399,3 +400,46 @@ def _add_response_params(self, section, shape):
shape,
include=[self._GENERIC_ERROR_SHAPE],
)


class ClientContextParamsDocumenter:
_CONFIG_GUIDE_LINK = (
'https://boto3.amazonaws.com/'
'v1/documentation/api/latest/guide/configuration.html'
)

def __init__(self, service_name, context_params):
self._service_name = service_name
self._context_params = context_params

def document_context_params(self, section):
self._add_title(section)
self._add_overview(section)
self._add_context_params_list(section)

def _add_title(self, section):
section.style.h2('Client Context Parameters')

def _add_overview(self, section):
section.style.new_line()
section.write(
'Client context parameters are configurable on a client '
'instance via the ``client_context_params`` parameter in the '
'``Config`` object. For more detailed instructions and examples '
'on the exact usage of context params see the '
)
section.style.external_link(
title='configuration guide',
link=self._CONFIG_GUIDE_LINK,
)
section.write('.')
section.style.new_line()

def _add_context_params_list(self, section):
section.style.new_line()
sn = f'``{self._service_name}``'
section.writeln(f'The available {sn} client context params are:')
for param in self._context_params:
section.style.new_line()
name = f'``{xform_name(param.name)}``'
section.write(f'* {name} ({param.type}) - {param.documentation}')
34 changes: 33 additions & 1 deletion botocore/docs/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from botocore.docs.bcdoc.restdoc import DocumentStructure
from botocore.docs.client import ClientDocumenter, ClientExceptionsDocumenter
from botocore.docs.client import (
ClientContextParamsDocumenter,
ClientDocumenter,
ClientExceptionsDocumenter,
)
from botocore.docs.paginator import PaginatorDocumenter
from botocore.docs.waiter import WaiterDocumenter
from botocore.exceptions import DataNotFoundError

OMITTED_CONTEXT_PARAMS = {
's3': (
'Accelerate',
'DisableMultiRegionAccessPoints',
'ForcePathStyle',
'UseArnRegion',
),
's3control': ('UseArnRegion',),
}


class ServiceDocumenter:
def __init__(self, service_name, session, root_docs_path):
Expand All @@ -37,6 +51,7 @@ def __init__(self, service_name, session, root_docs_path):
'client-exceptions',
'paginator-api',
'waiter-api',
'client-context-params',
]

def document_service(self):
Expand All @@ -52,6 +67,10 @@ def document_service(self):
self.client_exceptions(doc_structure.get_section('client-exceptions'))
self.paginator_api(doc_structure.get_section('paginator-api'))
self.waiter_api(doc_structure.get_section('waiter-api'))
context_params_section = doc_structure.get_section(
'client-context-params'
)
self.client_context_params(context_params_section)
return doc_structure.flush_structure()

def title(self, section):
Expand Down Expand Up @@ -108,3 +127,16 @@ def get_examples(self, service_name, api_version=None):
service_name, 'examples-1', api_version
)
return examples['examples']

def client_context_params(self, section):
params_to_omit = OMITTED_CONTEXT_PARAMS.get(self._service_name, [])
service_model = self._client.meta.service_model
raw_context_params = service_model.client_context_parameters
context_params = [
p for p in raw_context_params if p.name not in params_to_omit
]
if context_params:
context_param_documenter = ClientContextParamsDocumenter(
self._service_name, context_params
)
context_param_documenter.document_context_params(section)
21 changes: 20 additions & 1 deletion tests/functional/docs/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from botocore.docs.service import ServiceDocumenter
from botocore import xform_name
from botocore.docs.service import OMITTED_CONTEXT_PARAMS, ServiceDocumenter
from tests.functional.docs import BaseDocsFunctionalTest


Expand Down Expand Up @@ -78,3 +79,21 @@ def test_copy_source_param_docs_also_modified(self):
self.assert_contains_line(
"You can also provide this value as a dictionary", param_docs
)

def test_s3_context_params_omitted(self):
omitted_params = OMITTED_CONTEXT_PARAMS['s3']
content = ServiceDocumenter(
's3', self._session, self.root_services_path
).document_service()
for param in omitted_params:
param_name = f'``{xform_name(param)}``'
self.assert_not_contains_line(param_name, content)

def test_s3control_context_params_omitted(self):
omitted_params = OMITTED_CONTEXT_PARAMS['s3control']
content = ServiceDocumenter(
's3control', self._session, self.root_services_path
).document_service()
for param in omitted_params:
param_name = f'``{xform_name(param)}``'
self.assert_not_contains_line(param_name, content)
91 changes: 64 additions & 27 deletions tests/functional/test_context_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,86 +224,130 @@
**FAKE_MODEL_WITH_CLIENT_CONTEXT_PARAM,
"metadata": S3CONTROL_METADATA,
}
CLIENT_CONTEXT_PARAM_INPUT = {
"foo_client_context_param_name": "foo_context_param_value"
}
CONFIG_WITH_S3 = Config(s3=CLIENT_CONTEXT_PARAM_INPUT)
CONFIG_WITH_CLIENT_CONTEXT_PARAMS = Config(
client_context_params=CLIENT_CONTEXT_PARAM_INPUT
)
NO_CTX_PARAM_EXPECTED_CALL_KWARGS = {"Region": "us-east-1"}
CTX_PARAM_EXPECTED_CALL_KWARGS = {
**NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
"FooClientContextParamName": "foo_context_param_value",
}


@pytest.mark.parametrize(
'service_name,service_model,ruleset,call_should_include_ctx_param',
'service_name,service_model,ruleset,config,expected_call_kwargs',
[
# s3
(
's3',
FAKE_S3_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
True,
CONFIG_WITH_S3,
CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3',
FAKE_S3_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITHOUT_ANY_CONTEXT_PARAMS,
False,
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3',
FAKE_S3_MODEL_WITHOUT_ANY_CONTEXT_PARAMS,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
False,
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3',
FAKE_S3_MODEL_WITHOUT_ANY_CONTEXT_PARAMS,
FAKE_RULESET_WITHOUT_ANY_CONTEXT_PARAMS,
False,
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3',
FAKE_S3_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
CONFIG_WITH_CLIENT_CONTEXT_PARAMS,
CTX_PARAM_EXPECTED_CALL_KWARGS,
),
# s3control
(
's3control',
FAKE_S3CONTROL_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
True,
CONFIG_WITH_S3,
CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3control',
FAKE_S3CONTROL_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITHOUT_ANY_CONTEXT_PARAMS,
False,
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3control',
FAKE_S3CONTROL_MODEL_WITHOUT_ANY_CONTEXT_PARAMS,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
False,
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
's3control',
FAKE_S3CONTROL_MODEL_WITHOUT_ANY_CONTEXT_PARAMS,
FAKE_RULESET_WITHOUT_ANY_CONTEXT_PARAMS,
False,
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
# botocore does not currently support client context params for
# services other than s3 and s3-control.
(
's3control',
FAKE_S3CONTROL_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
CONFIG_WITH_CLIENT_CONTEXT_PARAMS,
CTX_PARAM_EXPECTED_CALL_KWARGS,
),
# otherservice
(
'otherservice',
FAKE_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
False, # would be True for s3 and s3control
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
'otherservice',
FAKE_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITHOUT_ANY_CONTEXT_PARAMS,
False, # same as for s3 and s3control
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
'otherservice',
FAKE_MODEL_WITHOUT_ANY_CONTEXT_PARAMS,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
False, # same as for s3 and s3control
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
'otherservice',
FAKE_MODEL_WITHOUT_ANY_CONTEXT_PARAMS,
FAKE_RULESET_WITHOUT_ANY_CONTEXT_PARAMS,
False, # same as for s3 and s3control
CONFIG_WITH_S3,
NO_CTX_PARAM_EXPECTED_CALL_KWARGS,
),
(
'otherservice',
FAKE_MODEL_WITH_CLIENT_CONTEXT_PARAM,
FAKE_RULESET_WITH_CLIENT_CONTEXT_PARAM,
CONFIG_WITH_CLIENT_CONTEXT_PARAMS,
CTX_PARAM_EXPECTED_CALL_KWARGS,
),
],
)
Expand All @@ -313,21 +357,20 @@ def test_client_context_param_sent_to_endpoint_resolver(
service_name,
service_model,
ruleset,
call_should_include_ctx_param,
config,
expected_call_kwargs,
):
# patch loader to return fake service model and fake endpoint ruleset
patch_load_service_model(
patched_session, monkeypatch, service_model, ruleset
)

# construct client using patched loader and a config object with an s3
# section that sets the foo_context_param to a value
# or client_context_param section that sets the foo_context_param to a value
client = patched_session.create_client(
service_name,
region_name='us-east-1',
config=Config(
s3={'foo_client_context_param_name': 'foo_context_param_value'}
),
config=config,
)

# Stub client to prevent a request from getting sent and ascertain that
Expand All @@ -342,13 +385,7 @@ def test_client_context_param_sent_to_endpoint_resolver(
) as mock_resolve_endpoint:
client.mock_operation(MockOpParam='mock-op-param-value')

if call_should_include_ctx_param:
mock_resolve_endpoint.assert_called_once_with(
Region='us-east-1',
FooClientContextParamName='foo_context_param_value',
)
else:
mock_resolve_endpoint.assert_called_once_with(Region='us-east-1')
mock_resolve_endpoint.assert_called_once_with(**expected_call_kwargs)


@pytest.mark.parametrize(
Expand Down
Loading