Skip to content

Commit 541a5f0

Browse files
author
j.s@google.com
committed
Improving debuggability of mock http core by adding dump request/response and detecting if the most recent response came from cache or the live server.
1 parent d7d72fa commit 541a5f0

5 files changed

Lines changed: 149 additions & 7 deletions

File tree

src/atom/client.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@
2525
import atom.http_core
2626

2727

28+
class Error(Exception):
29+
pass
30+
31+
32+
class MissingHost(Error):
33+
pass
34+
35+
2836
class AtomPubClient(object):
2937
host = None
3038
auth_token = None
@@ -67,6 +75,10 @@ def request(self, method=None, uri=None, auth_token=None,
6775
http_request:
6876
auth_token: An authorization token object whose modify_request method
6977
sets the HTTP Authorization header.
78+
79+
Returns:
80+
The results of calling self.http_client.request. With the default
81+
http_client, this is an HTTP response object.
7082
"""
7183
# Modify the request based on the AtomPubClient settings and parameters
7284
# passed in to the request.
@@ -96,48 +108,75 @@ def request(self, method=None, uri=None, auth_token=None,
96108
auth_token.modify_request(http_request)
97109
elif self.auth_token:
98110
self.auth_token.modify_request(http_request)
111+
# Check to make sure there is a host in the http_request.
112+
if http_request.uri.host is None:
113+
raise MissingHost('No host provided in request %s %s' % (
114+
http_request.method, str(http_request.uri)))
99115
# Perform the fully specified request using the http_client instance.
100116
# Sends the request to the server and returns the server's response.
101117
return self.http_client.request(http_request)
102118

103119
Request = request
104120

105121
def get(self, uri=None, auth_token=None, http_request=None, **kwargs):
122+
"""Performs a request using the GET method, returns an HTTP response."""
106123
return self.request(method='GET', uri=uri, auth_token=auth_token,
107124
http_request=http_request, **kwargs)
108125

109126
Get = get
110127

111128
def post(self, uri=None, data=None, auth_token=None, http_request=None,
112129
**kwargs):
130+
"""Sends data using the POST method, returns an HTTP response."""
113131
return self.request(method='POST', uri=uri, auth_token=auth_token,
114132
http_request=http_request, data=data, **kwargs)
115133

116134
Post = post
117135

118136
def put(self, uri=None, data=None, auth_token=None, http_request=None,
119137
**kwargs):
138+
"""Sends data using the PUT method, returns an HTTP response."""
120139
return self.request(method='PUT', uri=uri, auth_token=auth_token,
121140
http_request=http_request, data=data, **kwargs)
122141

123142
Put = put
124143

125144
def delete(self, uri=None, auth_token=None, http_request=None, **kwargs):
145+
"""Performs a request using the DELETE method, returns an HTTP response."""
126146
return self.request(method='DELETE', uri=uri, auth_token=auth_token,
127147
http_request=http_request, **kwargs)
128148

129149
Delete = delete
130150

131151
def modify_request(self, http_request):
152+
"""Changes the HTTP request before sending it to the server.
153+
154+
Sets the User-Agent HTTP header and fills in the HTTP host portion
155+
of the URL if one was not included in the request (for this it uses
156+
the self.host member if one is set). This method is called in
157+
self.request.
158+
159+
Args:
160+
http_request: An atom.http_core.HttpRequest() (optional) If one is
161+
not provided, a new HttpRequest is instantiated.
162+
163+
Returns:
164+
An atom.http_core.HttpRequest() with the User-Agent header set and
165+
if this client has a value in its host member, the host in the request
166+
URL is set.
167+
"""
132168
if http_request is None:
133169
http_request = atom.http_core.HttpRequest()
170+
134171
if self.host is not None and http_request.uri.host is None:
135172
http_request.uri.host = self.host
173+
136174
# Set the user agent header for logging purposes.
137175
if self.source:
138-
http_request.headers['User-Agent'] = '%s gdata-py/2.0.5' % self.source
176+
http_request.headers['User-Agent'] = '%s gdata-py/2.0.6' % self.source
139177
else:
140-
http_request.headers['User-Agent'] = 'gdata-py/2.0.5'
178+
http_request.headers['User-Agent'] = 'gdata-py/2.0.6'
179+
141180
return http_request
142181

143182
ModifyRequest = modify_request

src/atom/http_core.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,26 @@ def _copy(self):
174174
new_request._body_parts = self._body_parts[:]
175175
return new_request
176176

177+
def _dump(self):
178+
"""Converts to a printable string for debugging purposes.
179+
180+
In order to preserve the request, it does not read from file-like objects
181+
in the body.
182+
"""
183+
output = 'HTTP Request\n method: %s\n url: %s\n headers:\n' % (
184+
self.method, str(self.uri))
185+
for header, value in self.headers.iteritems():
186+
output += ' %s: %s\n' % (header, value)
187+
output += ' body sections:\n'
188+
i = 0
189+
for part in self._body_parts:
190+
if isinstance(part, (str, unicode)):
191+
output += ' %s: %s\n' % (i, part)
192+
else:
193+
output += ' %s: <file like object>\n' % i
194+
i += 1
195+
return output
196+
177197

178198
def _apply_defaults(http_request):
179199
if http_request.uri.scheme is None:
@@ -350,6 +370,23 @@ def read(self, amt=None):
350370
return self._body.read(amt)
351371

352372

373+
def _dump_response(http_response):
374+
"""Converts to a string for printing debug messages.
375+
376+
Does not read the body since that may consume the content.
377+
"""
378+
output = 'HttpResponse\n status: %s\n reason: %s\n headers:' % (
379+
http_response.status, http_response.reason)
380+
headers = http_response.getheaders()
381+
if isinstance(headers, dict):
382+
for header, value in headers.iteritems():
383+
output += ' %s: %s\n' % (header, value)
384+
else:
385+
for pair in headers:
386+
output += ' %s: %s\n' % (pair[0], pair[1])
387+
return output
388+
389+
353390
class HttpClient(object):
354391
"""Performs HTTP requests using httplib."""
355392
debug = None
@@ -395,6 +432,7 @@ def _http_request(self, method, uri, headers=None, body_parts=None):
395432
"""
396433
if isinstance(uri, (str, unicode)):
397434
uri = Uri.parse_uri(uri)
435+
398436
connection = self._get_connection(uri, headers=headers)
399437

400438
if self.debug:

src/atom/mock_http_core.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@
2828
import atom.http_core
2929

3030

31+
class Error(Exception):
32+
pass
33+
34+
35+
class NoRecordingFound(Error):
36+
pass
37+
38+
3139
class MockHttpClient(object):
3240
debug = None
3341
real_client = None
42+
last_request_was_live = False
3443

3544
# The following members are used to construct the session cache temp file
3645
# name.
@@ -64,20 +73,25 @@ def request(self, http_request):
6473
request = http_request._copy()
6574
_scrub_request(request)
6675
if self.real_client is None:
76+
self.last_request_was_live = False
6777
for recording in self._recordings:
6878
if _match_request(recording[0], request):
6979
return recording[1]
7080
else:
7181
# Pass along the debug settings to the real client.
7282
self.real_client.debug = self.debug
7383
# Make an actual request since we can use the real HTTP client.
84+
self.last_request_was_live = True
7485
response = self.real_client.request(http_request)
75-
_scrub_response(response)
76-
self.add_response(request, response.status, response.reason,
77-
dict(response.getheaders()), response.read())
86+
scrubbed_response = _scrub_response(response)
87+
self.add_response(request, scrubbed_response.status,
88+
scrubbed_response.reason,
89+
dict(scrubbed_response.getheaders()),
90+
scrubbed_response.read())
7891
# Return the recording which we just added.
7992
return self._recordings[-1][1]
80-
return None
93+
raise NoRecordingFound('No recoding was found for request: %s %s' % (
94+
request.method, str(request.uri)))
8195

8296
Request = request
8397

@@ -148,6 +162,18 @@ def get_cache_file_name(self):
148162
return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name,
149163
self.cache_test_name)
150164

165+
def _dump(self):
166+
"""Provides debug information in a string."""
167+
output = 'MockHttpClient\n real_client: %s\n cache file name: %s\n' % (
168+
self.real_client, self.get_cache_file_name())
169+
output += ' recordings:\n'
170+
i = 0
171+
for recording in self._recordings:
172+
output += ' recording %i is for: %s %s\n' % (
173+
i, recording[0].method, str(recording[0].uri))
174+
i += 1
175+
return output
176+
151177

152178
def _match_request(http_request, stored_request):
153179
"""Determines whether a request is similar enough to a stored request

src/gdata/docs/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ def upload(self, media, title, folder_or_uri=None, content_type=None,
365365
Returns:
366366
A gdata.docs.data.DocsEntry containing information about uploaded doc.
367367
"""
368+
uri = None
368369
if folder_or_uri is not None:
369370
if isinstance(folder_or_uri, gdata.docs.data.DocsEntry):
370371
# Verify that we're uploading the resource into to a folder.

src/gdata/gauth.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ def generate_auth_sub_url(next, scopes, secure=False, session=True,
209209
next: atom.http_core.Uri or string The URL user will be sent to after
210210
authorizing this web application to access their data.
211211
scopes: list containint strings or atom.http_core.Uri objects. The URLs
212-
of the services to be accessed.
212+
of the services to be accessed. Could also be a single string
213+
or single atom.http_core.Uri for requesting just one scope.
213214
secure: boolean (optional) Determines whether or not the issued token
214215
is a secure token.
215216
session: boolean (optional) Determines whether or not the issued token
@@ -234,6 +235,10 @@ def generate_auth_sub_url(next, scopes, secure=False, session=True,
234235
"""
235236
if isinstance(next, (str, unicode)):
236237
next = atom.http_core.Uri.parse_uri(next)
238+
# If the user passed in a string instead of a list for scopes, convert to
239+
# a single item tuple.
240+
if isinstance(scopes, (str, unicode, atom.http_core.Uri)):
241+
scopes = (scopes,)
237242
scopes_string = ' '.join([str(scope) for scope in scopes])
238243
next.query[scopes_param_prefix] = scopes_string
239244

@@ -1075,6 +1080,25 @@ def load_tokens(blob):
10751080

10761081

10771082
def ae_save(token, token_key):
1083+
"""Stores an auth token in the App Engine datastore.
1084+
1085+
This is a convenience method for using the library with App Engine.
1086+
Recommended usage is to associate the auth token with the current_user.
1087+
If a user is signed in to the app using the App Engine users API, you
1088+
can use
1089+
gdata.gauth.ae_save(some_token, users.get_current_user().user_id())
1090+
If you are not using the Users API you are free to choose whatever
1091+
string you would like for a token_string.
1092+
1093+
Args:
1094+
token: an auth token object. Must be one of ClientLoginToken,
1095+
AuthSubToken, SecureAuthSubToken, OAuthRsaToken, or OAuthHmacToken
1096+
(see token_to_blob).
1097+
token_key: str A unique identified to be used when you want to retrieve
1098+
the token. If the user is signed in to App Engine using the
1099+
users API, I recommend using the user ID for the token_key:
1100+
users.get_current_user().user_id()
1101+
"""
10781102
import gdata.alt.app_engine
10791103
key_name = ''.join(('gd_auth_token', token_key))
10801104
return gdata.alt.app_engine.set_token(key_name, token_to_blob(token))
@@ -1084,6 +1108,19 @@ def ae_save(token, token_key):
10841108

10851109

10861110
def ae_load(token_key):
1111+
"""Retrieves a token object from the App Engine datastore.
1112+
1113+
This is a convenience method for using the library with App Engine.
1114+
See also ae_save.
1115+
1116+
Args:
1117+
token_key: str The unique key associated with the desired token when it
1118+
was saved using ae_save.
1119+
1120+
Returns:
1121+
A token object if there was a token associated with the token_key or None
1122+
if the key could not be found.
1123+
"""
10871124
import gdata.alt.app_engine
10881125
key_name = ''.join(('gd_auth_token', token_key))
10891126
token_string = gdata.alt.app_engine.get_token(key_name)
@@ -1097,6 +1134,7 @@ def ae_load(token_key):
10971134

10981135

10991136
def ae_delete(token_key):
1137+
"""Removes the token object from the App Engine datastore."""
11001138
import gdata.alt.app_engine
11011139
key_name = ''.join(('gd_auth_token', token_key))
11021140
gdata.alt.app_engine.delete_token(key_name)

0 commit comments

Comments
 (0)