Skip to content
This repository was archived by the owner on Apr 19, 2026. It is now read-only.
Merged
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
12 changes: 12 additions & 0 deletions endpoints/api_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ def _CheckLimitDefinitions(limit_definitions):
_CheckType(ld.default_limit, int, 'limit_definition.default_limit')


_VALID_API_NAME = re.compile('^[a-z][a-z0-9]{0,39}$')


def _CheckApiName(name):
valid = (_VALID_API_NAME.match(name) is not None)
if not valid:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe compact to if not _VALID_API_NAME.match(name)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8 recommends being explicit about checking if a value is None.

raise api_exceptions.InvalidApiNameException(
'The API name must match the regular expression {}'.format(
_VALID_API_NAME.pattern[1:-1]))


# pylint: disable=g-bad-name
class _ApiInfo(object):
"""Configurable attributes of an API.
Expand Down Expand Up @@ -581,6 +592,7 @@ def __init__(self, name, version, description=None, hostname=None,
limit_definitions: list of LimitDefinition tuples used in this API.
"""
_CheckType(name, basestring, 'name', allow_none=False)
_CheckApiName(name)
_CheckType(version, basestring, 'version', allow_none=False)
_CheckType(description, basestring, 'description')
_CheckType(hostname, basestring, 'hostname')
Expand Down
4 changes: 4 additions & 0 deletions endpoints/api_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,9 @@ class InvalidLimitDefinitionException(Exception):
"""Exception thrown if there's an invalid rate limit definition."""


class InvalidApiNameException(Exception):
"""Exception thrown if the api name does not match the required character set."""


class ToolError(Exception):
"""Exception thrown if there's a general error in the endpointscfg.py tool."""
1 change: 1 addition & 0 deletions endpoints/openapi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ def get_descriptor_defaults(self, api_info, hostname=None):
'version': api_info.api_version,
'title': api_info.name
},
'x-google-api-name': api_info.name,
'host': hostname,
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down
60 changes: 41 additions & 19 deletions endpoints/test/api_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import itertools
import json
import unittest
import pytest

import endpoints.api_config as api_config
from endpoints.api_config import ApiConfigGenerator
Expand Down Expand Up @@ -1974,7 +1975,7 @@ class ApiDecoratorTest(unittest.TestCase):

def testApiInfoPopulated(self):

@api_config.api(name='CoolService', version='vX',
@api_config.api(name='coolservice', version='vX',
description='My Cool Service', hostname='myhost.com',
canonical_name='Cool Service Name',
namespace=api_config.Namespace('domain', 'name', 'path'))
Expand All @@ -1983,7 +1984,7 @@ class MyDecoratedService(remote.Service):
pass

api_info = MyDecoratedService.api_info
self.assertEqual('CoolService', api_info.name)
self.assertEqual('coolservice', api_info.name)
self.assertEqual('vX', api_info.api_version)
self.assertEqual('vX', api_info.path_version)
self.assertEqual('My Cool Service', api_info.description)
Expand All @@ -2002,13 +2003,13 @@ class MyDecoratedService(remote.Service):

def testApiInfoDefaults(self):

@api_config.api('CoolService2', 'v2')
@api_config.api('coolservice2', 'v2')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
pass

api_info = MyDecoratedService.api_info
self.assertEqual('CoolService2', api_info.name)
self.assertEqual('coolservice2', api_info.name)
self.assertEqual('v2', api_info.api_version)
self.assertEqual('v2', api_info.path_version)
self.assertEqual(None, api_info.description)
Expand All @@ -2021,7 +2022,7 @@ class MyDecoratedService(remote.Service):
def testApiInfoInvalidNamespaceNoDomain(self):

with self.assertRaises(api_exceptions.InvalidNamespaceException):
@api_config.api('CoolService2', 'v2',
@api_config.api('coolservice2', 'v2',
namespace=api_config.Namespace(None, 'name', 'path'))
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
Expand All @@ -2030,15 +2031,15 @@ class MyDecoratedService(remote.Service):
def testApiInfoInvalidNamespaceNoName(self):

with self.assertRaises(api_exceptions.InvalidNamespaceException):
@api_config.api('CoolService2', 'v2',
@api_config.api('coolservice2', 'v2',
namespace=api_config.Namespace('domain', None, 'path'))
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
pass

def testApiInfoNamespaceDefaultPath(self):

@api_config.api('CoolService2', 'v2',
@api_config.api('coolservice2', 'v2',
namespace=api_config.Namespace('domain', 'name', None))
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
Expand All @@ -2051,7 +2052,7 @@ class MyDecoratedService(remote.Service):

def testGetApiClassesSingle(self):
"""Test that get_api_classes works when one class has been decorated."""
my_api = api_config.api(name='My Service', version='v1')
my_api = api_config.api(name='myservice', version='v1')

@my_api
class MyDecoratedService(remote.Service):
Expand All @@ -2061,7 +2062,7 @@ class MyDecoratedService(remote.Service):

def testGetApiClassesSingleCollection(self):
"""Test that get_api_classes works with the collection() decorator."""
my_api = api_config.api(name='My Service', version='v1')
my_api = api_config.api(name='myservice', version='v1')

@my_api.api_class(resource_name='foo')
class MyDecoratedService(remote.Service):
Expand All @@ -2071,7 +2072,7 @@ class MyDecoratedService(remote.Service):

def testGetApiClassesMultiple(self):
"""Test that get_api_classes works with multiple classes."""
my_api = api_config.api(name='My Service', version='v1')
my_api = api_config.api(name='myservice', version='v1')

@my_api.api_class(resource_name='foo')
class MyDecoratedService1(remote.Service):
Expand All @@ -2090,7 +2091,7 @@ class MyDecoratedService3(remote.Service):

def testGetApiClassesMixedStyles(self):
"""Test that get_api_classes works when decorated differently."""
my_api = api_config.api(name='My Service', version='v1')
my_api = api_config.api(name='myservice', version='v1')

# @my_api is equivalent to @my_api.api_class(). This is allowed, though
# mixing styles like this shouldn't be encouraged.
Expand All @@ -2109,6 +2110,27 @@ class MyDecoratedService3(remote.Service):
self.assertEqual([MyDecoratedService1, MyDecoratedService2,
MyDecoratedService3], my_api.get_api_classes())

def testApiNameRestrictions(self):

@api_config.api(name='coolservice', version='vX')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
pass

api_info = MyDecoratedService.api_info
assert 'coolservice' == api_info.name

with pytest.raises(api_exceptions.InvalidApiNameException):
@api_config.api('CoolService2', 'v2')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
pass

with pytest.raises(api_exceptions.InvalidApiNameException):
@api_config.api('c' + 'o'*40 + 'l', 'v2')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
pass

class MethodDecoratorTest(unittest.TestCase):

Expand Down Expand Up @@ -2180,7 +2202,7 @@ def _several_underscores__in_various___places__(self):

def testMethodInfoPopulated(self):

@api_config.api(name='CoolService', version='vX',
@api_config.api(name='coolservice', version='vX',
description='My Cool Service', hostname='myhost.com')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""
Expand Down Expand Up @@ -2211,7 +2233,7 @@ def my_method(self):

def testMethodInfoDefaults(self):

@api_config.api('CoolService2', 'v2')
@api_config.api('coolservice2', 'v2')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""

Expand Down Expand Up @@ -2241,7 +2263,7 @@ class MyRequest(messages.Message):
dog = messages.StringField(3)
panda = messages.StringField(4, required=True)

@api_config.api('CoolService3', 'v3')
@api_config.api('coolservice3', 'v3')
class MyDecoratedService(remote.Service):
"""Describes MyDecoratedService."""

Expand Down Expand Up @@ -2346,7 +2368,7 @@ def TryListAttributeVariations(self, attribute_name, config_name,
api_kwargs = {attribute_name: api_value}
method_kwargs = {attribute_name: method_value}

@api_config.api('AuthService', 'v1', hostname='example.appspot.com',
@api_config.api('authservice', 'v1', hostname='example.appspot.com',
**api_kwargs)
class AuthServiceImpl(remote.Service):
"""Describes AuthServiceImpl."""
Expand All @@ -2363,7 +2385,7 @@ def baz(self):
generator = ApiConfigGenerator()
api = json.loads(generator.pretty_print_config_to_json(AuthServiceImpl))
expected = {
'authService.baz': {
'authservice.baz': {
'httpMethod': 'POST',
'path': 'baz',
'request': {'body': 'empty'},
Expand All @@ -2375,9 +2397,9 @@ def baz(self):
}
}
if expected_value:
expected['authService.baz'][config_name] = expected_value
elif config_name in expected['authService.baz']:
del expected['authService.baz'][config_name]
expected['authservice.baz'][config_name] = expected_value
elif config_name in expected['authservice.baz']:
del expected['authservice.baz'][config_name]

test_util.AssertDictEqual(expected, api['methods'], self)

Expand Down
2 changes: 1 addition & 1 deletion endpoints/test/apiserving_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def delete(self, unused_request):
return message_types.VoidMessage()


my_api = api_config.api(name='My Service', version='v1')
my_api = api_config.api(name='myservice', version='v1')


@my_api.api_class()
Expand Down
16 changes: 16 additions & 0 deletions endpoints/test/openapi_generator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def entries_post_audiences(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -480,6 +481,7 @@ def items_put_container(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1031,6 +1033,7 @@ def get_airport_2(self, request):
'title': 'iata',
'version': 'v1',
},
'x-google-api-name': 'iata',
'host': None,
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1124,6 +1127,7 @@ def noop_get(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'localhost:8080',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1187,6 +1191,7 @@ def noop_get(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1287,6 +1292,7 @@ def override_get(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1359,6 +1365,7 @@ def noop_get(self, unused_request):
'description': 'Describes MyService.',
'version': '1.3.4',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1411,6 +1418,7 @@ def noop_get(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1463,6 +1471,7 @@ def noop_get(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1515,6 +1524,7 @@ def toplevel(self, unused_request):
'description': 'Testing repeated params',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1596,6 +1606,7 @@ def toplevel(self, unused_request):
'description': 'Testing repeated simple field params',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1671,6 +1682,7 @@ def toplevel(self, unused_request):
'description': 'Testing repeated Message params',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1768,6 +1780,7 @@ def noop_get(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -1833,6 +1846,7 @@ def entries_post_audience(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -2030,6 +2044,7 @@ def entries_post(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down Expand Up @@ -2116,6 +2131,7 @@ def entries_post(self, unused_request):
'description': 'Describes MyService.',
'version': 'v1',
},
'x-google-api-name': 'root',
'host': 'example.appspot.com',
'consumes': ['application/json'],
'produces': ['application/json'],
Expand Down
6 changes: 3 additions & 3 deletions endpoints/test/users_id_token_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ class UsersIdTokenTestWithSimpleApi(UsersIdTokenTestBase):

# pylint: disable=g-bad-name

@api_config.api('TestApi', 'v1')
@api_config.api('testapi', 'v1')
class TestApiAnnotatedAtMethod(remote.Service):
"""Describes TestApi."""

Expand All @@ -563,7 +563,7 @@ def method(self):
pass

@api_config.api(
'TestApi', 'v1', audiences=UsersIdTokenTestBase._SAMPLE_AUDIENCES,
'testapi', 'v1', audiences=UsersIdTokenTestBase._SAMPLE_AUDIENCES,
allowed_client_ids=UsersIdTokenTestBase._SAMPLE_ALLOWED_CLIENT_IDS)
class TestApiAnnotatedAtApi(remote.Service):
"""Describes TestApi."""
Expand Down Expand Up @@ -639,7 +639,7 @@ def testMaybeSetVarsWithActualRequestAccessToken(self, mock_local, mock_get_clie
dummy_email = 'test@gmail.com'
dummy_client_id = self._SAMPLE_ALLOWED_CLIENT_IDS[0]

@api_config.api('TestApi', 'v1',
@api_config.api('testapi', 'v1',
allowed_client_ids=self._SAMPLE_ALLOWED_CLIENT_IDS,
scopes=[dummy_scope])
class TestApiScopes(remote.Service):
Expand Down