Skip to content

Commit 20af04d

Browse files
tcoffee-googleJon Wayne Parrott
authored and
Jon Wayne Parrott
committed
Fix generation for methods with abnormal page token conventions (googleapis#330)
* Fix generation for methods with abnormal page token conventions Addresses googleapis/gapic-generator#692
1 parent afe134b commit 20af04d

File tree

7 files changed

+7444
-35
lines changed

7 files changed

+7444
-35
lines changed

googleapiclient/discovery.py

+84-32
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
'type': 'string',
118118
'required': False,
119119
}
120+
_PAGE_TOKEN_NAMES = ('pageToken', 'nextPageToken')
120121

121122
# Parameters accepted by the stack, but not visible via discovery.
122123
# TODO(dhermes): Remove 'userip' in 'v2'.
@@ -724,7 +725,11 @@ def method(self, **kwargs):
724725

725726
for name in parameters.required_params:
726727
if name not in kwargs:
727-
raise TypeError('Missing required parameter "%s"' % name)
728+
# temporary workaround for non-paging methods incorrectly requiring
729+
# page token parameter (cf. drive.changes.watch vs. drive.changes.list)
730+
if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
731+
_methodProperties(methodDesc, schema, 'response')):
732+
raise TypeError('Missing required parameter "%s"' % name)
728733

729734
for name, regex in six.iteritems(parameters.pattern_params):
730735
if name in kwargs:
@@ -927,13 +932,20 @@ def method(self, **kwargs):
927932
return (methodName, method)
928933

929934

930-
def createNextMethod(methodName):
935+
def createNextMethod(methodName,
936+
pageTokenName='pageToken',
937+
nextPageTokenName='nextPageToken',
938+
isPageTokenParameter=True):
931939
"""Creates any _next methods for attaching to a Resource.
932940
933941
The _next methods allow for easy iteration through list() responses.
934942
935943
Args:
936944
methodName: string, name of the method to use.
945+
pageTokenName: string, name of request page token field.
946+
nextPageTokenName: string, name of response page token field.
947+
isPageTokenParameter: Boolean, True if request page token is a query
948+
parameter, False if request page token is a field of the request body.
937949
"""
938950
methodName = fix_method_name(methodName)
939951

@@ -951,24 +963,24 @@ def methodNext(self, previous_request, previous_response):
951963
# Retrieve nextPageToken from previous_response
952964
# Use as pageToken in previous_request to create new request.
953965

954-
if 'nextPageToken' not in previous_response or not previous_response['nextPageToken']:
966+
nextPageToken = previous_response.get(nextPageTokenName, None)
967+
if not nextPageToken:
955968
return None
956969

957970
request = copy.copy(previous_request)
958971

959-
pageToken = previous_response['nextPageToken']
960-
parsed = list(urlparse(request.uri))
961-
q = parse_qsl(parsed[4])
962-
963-
# Find and remove old 'pageToken' value from URI
964-
newq = [(key, value) for (key, value) in q if key != 'pageToken']
965-
newq.append(('pageToken', pageToken))
966-
parsed[4] = urlencode(newq)
967-
uri = urlunparse(parsed)
968-
969-
request.uri = uri
970-
971-
logger.info('URL being requested: %s %s' % (methodName,uri))
972+
if isPageTokenParameter:
973+
# Replace pageToken value in URI
974+
request.uri = _add_query_parameter(
975+
request.uri, pageTokenName, nextPageToken)
976+
logger.info('Next page request URL: %s %s' % (methodName, request.uri))
977+
else:
978+
# Replace pageToken value in request body
979+
model = self._model
980+
body = model.deserialize(request.body)
981+
body[pageTokenName] = nextPageToken
982+
request.body = model.serialize(body)
983+
logger.info('Next page request body: %s %s' % (methodName, body))
972984

973985
return request
974986

@@ -1116,19 +1128,59 @@ def methodResource(self):
11161128
method.__get__(self, self.__class__))
11171129

11181130
def _add_next_methods(self, resourceDesc, schema):
1119-
# Add _next() methods
1120-
# Look for response bodies in schema that contain nextPageToken, and methods
1121-
# that take a pageToken parameter.
1122-
if 'methods' in resourceDesc:
1123-
for methodName, methodDesc in six.iteritems(resourceDesc['methods']):
1124-
if 'response' in methodDesc:
1125-
responseSchema = methodDesc['response']
1126-
if '$ref' in responseSchema:
1127-
responseSchema = schema.get(responseSchema['$ref'])
1128-
hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
1129-
{})
1130-
hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
1131-
if hasNextPageToken and hasPageToken:
1132-
fixedMethodName, method = createNextMethod(methodName + '_next')
1133-
self._set_dynamic_attr(fixedMethodName,
1134-
method.__get__(self, self.__class__))
1131+
# Add _next() methods if and only if one of the names 'pageToken' or
1132+
# 'nextPageToken' occurs among the fields of both the method's response
1133+
# type either the method's request (query parameters) or request body.
1134+
if 'methods' not in resourceDesc:
1135+
return
1136+
for methodName, methodDesc in six.iteritems(resourceDesc['methods']):
1137+
nextPageTokenName = _findPageTokenName(
1138+
_methodProperties(methodDesc, schema, 'response'))
1139+
if not nextPageTokenName:
1140+
continue
1141+
isPageTokenParameter = True
1142+
pageTokenName = _findPageTokenName(methodDesc.get('parameters', {}))
1143+
if not pageTokenName:
1144+
isPageTokenParameter = False
1145+
pageTokenName = _findPageTokenName(
1146+
_methodProperties(methodDesc, schema, 'request'))
1147+
if not pageTokenName:
1148+
continue
1149+
fixedMethodName, method = createNextMethod(
1150+
methodName + '_next', pageTokenName, nextPageTokenName,
1151+
isPageTokenParameter)
1152+
self._set_dynamic_attr(fixedMethodName,
1153+
method.__get__(self, self.__class__))
1154+
1155+
1156+
def _findPageTokenName(fields):
1157+
"""Search field names for one like a page token.
1158+
1159+
Args:
1160+
fields: container of string, names of fields.
1161+
1162+
Returns:
1163+
First name that is either 'pageToken' or 'nextPageToken' if one exists,
1164+
otherwise None.
1165+
"""
1166+
return next((tokenName for tokenName in _PAGE_TOKEN_NAMES
1167+
if tokenName in fields), None)
1168+
1169+
def _methodProperties(methodDesc, schema, name):
1170+
"""Get properties of a field in a method description.
1171+
1172+
Args:
1173+
methodDesc: object, fragment of deserialized discovery document that
1174+
describes the method.
1175+
schema: object, mapping of schema names to schema descriptions.
1176+
name: string, name of top-level field in method description.
1177+
1178+
Returns:
1179+
Object representing fragment of deserialized discovery document
1180+
corresponding to 'properties' field of object corresponding to named field
1181+
in method description, if it exists, otherwise empty dict.
1182+
"""
1183+
desc = methodDesc.get(name, {})
1184+
if '$ref' in desc:
1185+
desc = schema.get(desc['$ref'], {})
1186+
return desc.get('properties', {})

googleapiclient/http.py

+1
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ def execute(self, http=None, num_retries=0):
817817
if 'content-length' not in self.headers:
818818
self.headers['content-length'] = str(self.body_size)
819819
# If the request URI is too long then turn it into a POST request.
820+
# Assume that a GET request never contains a request body.
820821
if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
821822
self.method = 'POST'
822823
self.headers['x-http-method-override'] = 'GET'

googleapiclient/schema.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,14 @@ def prettyPrintSchema(self, schema):
161161
# Return with trailing comma and newline removed.
162162
return self._prettyPrintSchema(schema, dent=1)[:-2]
163163

164-
def get(self, name):
164+
def get(self, name, default=None):
165165
"""Get deserialized JSON schema from the schema name.
166166
167167
Args:
168168
name: string, Schema name.
169+
default: object, return value if name not found.
169170
"""
170-
return self.schemas[name]
171+
return self.schemas.get(name, default)
171172

172173

173174
class _SchemaToStruct(object):

0 commit comments

Comments
 (0)