Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
129 changes: 129 additions & 0 deletions awscli/botocore/service_module_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# 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.
"""Service module names that differ from the name derived from ``serviceId``.
Keyed by ``serviceId``. An entry exists either because the service shipped
with that name before names were derived from ``serviceId``, or because the
derived name collided with an existing one.
Module names are public. Never change a value while the service ships;
remove an entry only when its models are removed from ``botocore/data``.
"""

SERVICE_MODULE_NAMES = {
'amp': 'PrometheusService',
'AppIntegrations': 'AppIntegrationsService',
'Application Signals': 'CloudWatchApplicationSignals',
'b2bi': 'B2BI',
'BCM Dashboards': 'BillingandCostManagementDashboards',
'BCM Data Exports': 'BillingandCostManagementDataExports',
'BCM Pricing Calculator': 'BillingandCostManagementPricingCalculator',
'BCM Recommended Actions': 'BillingandCostManagementRecommendedActions',
'Bedrock Agent': 'AgentsforBedrock',
'Bedrock Agent Runtime': 'AgentsforBedrockRuntime',
'Bedrock Data Automation': 'DataAutomationforBedrock',
'Bedrock Data Automation Runtime': 'RuntimeforBedrockDataAutomation',
'billingconductor': 'BillingConductor',
'chatbot': 'Chatbot',
'CleanRooms': 'CleanRoomsService',
'CloudControl': 'CloudControlApi',
'CloudTrail Data': 'CloudTrailDataService',
'codeartifact': 'CodeArtifact',
'codestar notifications': 'CodeStarNotifications',
'ConnectCampaigns': 'ConnectCampaignService',
'ConnectCampaignsV2': 'ConnectCampaignServiceV2',
'DataBrew': 'GlueDataBrew',
'deadline': 'DeadlineCloud',
'DevOps Agent': 'DevOpsAgentService',
'DSQL': 'AuroraDSQL',
'ECR PUBLIC': 'ECRPublic',
'EMR containers': 'EMRContainers',
'evs': 'EVS',
'fis': 'FIS',
'forecast': 'ForecastService',
'forecastquery': 'ForecastQueryService',
'Geo Maps': 'LocationServiceMapsV2',
'Geo Places': 'LocationServicePlacesV2',
'Geo Routes': 'LocationServiceRoutesV2',
'grafana': 'ManagedGrafana',
'IAM Toolbox': 'IAMToolboxPreview',
'identitystore': 'IdentityStore',
'Inspector Scan': 'inspectorscan',
'InternetMonitor': 'CloudWatchInternetMonitor',
'IoT Managed Integrations': 'ManagedintegrationsforIoTDeviceManagement',
'IotDeviceAdvisor': 'IoTDeviceAdvisor',
'ivs': 'IVS',
'IVS RealTime': 'ivsrealtime',
'Kinesis Video Signaling': 'KinesisVideoSignalingChannels',
'Lambda Microvms': 'LambdaMicroVMs',
'Location': 'LocationService',
'm2': 'MainframeModernization',
'Marketplace Agreement': 'AgreementService',
'Marketplace Deployment': 'MarketplaceDeploymentService',
'Marketplace Reporting': 'MarketplaceReportingService',
'MediaPackageV2': 'mediapackagev2',
'Medical Imaging': 'HealthImaging',
'MigrationHubStrategy': 'MigrationHubStrategyRecommendations',
'MPA': 'MultipartyApproval',
'mq': 'MQ',
'neptunedata': 'NeptuneData',
'Network Security Manager': 'NetworkSecurityManagerCustomerAPI',
'NetworkMonitor': 'CloudWatchNetworkMonitor',
'Notifications': 'UserNotifications',
'NotificationsContacts': 'UserNotificationsContacts',
'Nova Act': 'NovaActService',
'OAM': 'CloudWatchObservabilityAccessManager',
'ObservabilityAdmin': 'CloudWatchObservabilityAdminService',
'OpenSearch': 'OpenSearchService',
'OpenSearchServerless': 'OpenSearchServiceServerless',
'OSIS': 'OpenSearchIngestion',
'PartnerCentral Account': 'PartnerCentralAccountAPI',
'PartnerCentral Channel': 'PartnerCentralChannelAPI',
'PartnerCentral Revenue Measurement': 'PartnerCentralRevenueMeasurementAPI',
'PartnerCentral Selling': 'PartnerCentralSellingAPI',
'Payment Cryptography': 'PaymentCryptographyControlPlane',
'Payment Cryptography Data': 'PaymentCryptographyDataPlane',
'Pca Connector Scep': 'PrivateCAConnectorforSCEP',
'PCS': 'ParallelComputingService',
'Pipes': 'EventBridgePipes',
'rbin': 'RecycleBin',
'RDS Data': 'RDSDataService',
'Redshift Data': 'RedshiftDataAPIService',
'repostspace': 'rePostPrivate',
'resiliencehub': 'ResilienceHub',
'resiliencehubv2': 'ResilienceHubV2',
'Resource Explorer 2': 'ResourceExplorer',
'RolesAnywhere': 'IAMRolesAnywhere',
'RUM': 'CloudWatchRUM',
'SageMaker A2I Runtime': 'AugmentedAIRuntime',
'Sagemaker Edge': 'SagemakerEdgeManager',
'SageMaker Geospatial': 'SageMakergeospatialcapabilities',
'SagemakerJobRuntime': 'SagemakerJobRuntimeService',
'savingsplans': 'SavingsPlans',
'Scheduler': 'EventBridgeScheduler',
'schemas': 'Schemas',
'Security IR': 'SecurityIncidentResponse',
'Service Catalog AppRegistry': 'AppRegistry',
'SESv2': 'SESV2',
'Signer Data': 'SignerDataPlane',
'Signin': 'SignInService',
'SocialMessaging': 'EndUserMessagingSocial',
'SSM GuiConnect': 'SSMGUIConnect',
'SSM QuickSetup': 'SystemsManagerQuickSetup',
'synthetics': 'Synthetics',
'Transcribe': 'TranscribeService',
'TrustedAdvisor': 'TrustedAdvisorPublicAPI',
'uxc': 'UserExperienceCustomization',
'Wickr': 'WickrAdminAPI',
'Wisdom': 'ConnectWisdomService',
}
34 changes: 24 additions & 10 deletions awscli/botocore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
UnsupportedS3ControlArnError,
UnsupportedS3ControlConfigurationError,
)
from botocore.service_module_names import SERVICE_MODULE_NAMES
from botocore.useragent import register_feature_id
from dateutil.tz import tzutc
from urllib3.exceptions import LocationParseError
Expand Down Expand Up @@ -218,20 +219,33 @@ def has_header(header_name, headers):
return header_name.lower() in [key.lower() for key in headers.keys()]


_TABLED_MODULE_NAMES = frozenset(SERVICE_MODULE_NAMES.values())


def get_service_module_name(service_model):
"""Returns the module name for a service

This is the value used in both the documentation and client class name
This is the value used in both the documentation and client class name.

The name is taken from ``SERVICE_MODULE_NAMES`` when the ``serviceId``
has an entry there, otherwise derived from the ``serviceId`` itself.

:raises ValueError: if the derived name is already used by an entry in
``SERVICE_MODULE_NAMES``.
"""
name = service_model.metadata.get(
'serviceAbbreviation',
service_model.metadata.get(
'serviceFullName', service_model.service_name
),
)
name = name.replace('Amazon', '')
name = name.replace('AWS', '')
name = re.sub(r'\W+', '', name)
service_id = service_model.metadata.get('serviceId')
if service_id is None:
return service_model.service_name
if service_id in SERVICE_MODULE_NAMES:
return SERVICE_MODULE_NAMES[service_id]
name = re.sub(r'\W+', '', service_id)
if name in _TABLED_MODULE_NAMES:
raise ValueError(
f'serviceId {service_id!r} derives the module name {name!r}, '
f'which is already used by another service in '
f'SERVICE_MODULE_NAMES. Record a different name for '
f'{service_id!r} in that table.'
)
return name


Expand Down
91 changes: 91 additions & 0 deletions tests/functional/botocore/test_service_module_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# 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.
import copy

import pytest
from botocore.loaders import Loader
from botocore.model import ServiceModel
from botocore.service_module_names import SERVICE_MODULE_NAMES
from botocore.utils import get_service_module_name

LOADER = Loader()
DOCGEN_KEYS = ('serviceFullName', 'serviceAbbreviation')
SENTINEL = 'AWS Renamed Service'


def _all_service_models():
for service_name in LOADER.list_available_services('service-2'):
for api_version in LOADER.list_api_versions(service_name, 'service-2'):
yield service_name, api_version


def _load(service_name, api_version):
description = LOADER.load_service_model(
service_name, 'service-2', api_version
)
return ServiceModel(description, service_name)


def _with_docgen_sentinels(service_model):
description = copy.deepcopy(service_model._service_description)
for key in DOCGEN_KEYS:
description['metadata'][key] = SENTINEL
return ServiceModel(description, service_model.service_name)


@pytest.mark.parametrize(
'service_name, api_version', list(_all_service_models())
)
def test_module_name_ignores_docgen_ids(service_name, api_version):
model = _load(service_name, api_version)
expected = get_service_module_name(model)
actual = get_service_module_name(_with_docgen_sentinels(model))
assert actual == expected, (
f'{service_name}/{api_version}: module name changed from '
f'{expected!r} to {actual!r} when documentation ids changed'
)


@pytest.mark.parametrize(
'service_name, api_version', list(_all_service_models())
)
def test_every_shipped_model_has_service_id(service_name, api_version):
model = _load(service_name, api_version)
assert model.metadata.get('serviceId'), (
f'{service_name}/{api_version} has no serviceId; the module name '
f'would fall back to the model directory name'
)


def test_table_keys_are_shipped_service_ids():
shipped = {
_load(s, v).metadata.get('serviceId') for s, v in _all_service_models()
}
unknown = set(SERVICE_MODULE_NAMES) - shipped
assert not unknown, f'Table entries for unknown serviceIds: {unknown}'


def test_no_two_services_share_module_name():
service_ids_by_name = {}
for service_name, api_version in _all_service_models():
model = _load(service_name, api_version)
name = get_service_module_name(model)
service_ids_by_name.setdefault(name, set()).add(
model.metadata['serviceId']
)
collisions = {
name: sorted(service_ids)
for name, service_ids in service_ids_by_name.items()
if len(service_ids) > 1
}
assert not collisions, f'Module name collisions: {collisions}'
34 changes: 12 additions & 22 deletions tests/unit/botocore/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,45 +221,35 @@ def test_client_name(self):
service_client = creator.create_client(
'myservice', 'us-west-2', credentials=self.credentials
)
self.assertTrue(service_client.__class__.__name__, 'MyService')
self.assertEqual(service_client.__class__.__name__, 'MyService')

def test_client_name_with_amazon(self):
self.service_description['metadata']['serviceFullName'] = (
'Amazon MyService'
)
def test_client_name_from_service_id_with_spaces(self):
self.service_description['metadata']['serviceId'] = 'My Big Service'
creator = self.create_client_creator()
service_client = creator.create_client(
'myservice', 'us-west-2', credentials=self.credentials
)
self.assertTrue(service_client.__class__.__name__, 'MyService')
self.assertEqual(service_client.__class__.__name__, 'MyBigService')

def test_client_name_using_abreviation(self):
self.service_description['metadata']['serviceAbbreviation'] = (
'Abbreviation'
)
creator = self.create_client_creator()
service_client = creator.create_client(
'myservice', 'us-west-2', credentials=self.credentials
)
self.assertTrue(service_client.__class__.__name__, 'Abbreviation')

def test_client_name_with_non_alphabet_characters(self):
def test_client_name_ignores_full_name(self):
self.service_description['metadata']['serviceFullName'] = (
'Amazon My-Service'
'Amazon Renamed Service'
)
creator = self.create_client_creator()
service_client = creator.create_client(
'myservice', 'us-west-2', credentials=self.credentials
)
self.assertTrue(service_client.__class__.__name__, 'MyService')
self.assertEqual(service_client.__class__.__name__, 'MyService')

def test_client_name_with_no_full_name_or_abbreviation(self):
del self.service_description['metadata']['serviceFullName']
def test_client_name_ignores_abbreviation(self):
self.service_description['metadata']['serviceAbbreviation'] = (
'Abbreviation'
)
creator = self.create_client_creator()
service_client = creator.create_client(
'myservice', 'us-west-2', credentials=self.credentials
)
self.assertTrue(service_client.__class__.__name__, 'myservice')
self.assertEqual(service_client.__class__.__name__, 'MyService')

def test_client_generated_from_model(self):
creator = self.create_client_creator()
Expand Down
1 change: 1 addition & 0 deletions tests/unit/botocore/test_errorfactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def setUp(self):
self.model = {
"metadata": {
'endpointPrefix': 'myservice',
'serviceId': 'MyService',
'serviceFullName': 'MyService',
},
'operations': {
Expand Down
Loading
Loading