Merge lp:~leonardr/lazr.restfulclient/user-agent into lp:lazr.restfulclient

Proposed by Leonard Richardson
Status: Merged
Approved by: Aaron Bentley
Approved revision: 93
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restfulclient/user-agent
Merge into: lp:lazr.restfulclient
Diff against target: 241 lines (+89/-9)
7 files modified
src/lazr/restfulclient/NEWS.txt (+7/-1)
src/lazr/restfulclient/_browser.py (+4/-1)
src/lazr/restfulclient/authorize/__init__.py (+11/-0)
src/lazr/restfulclient/authorize/oauth.py (+10/-0)
src/lazr/restfulclient/docs/authorizer.txt (+18/-2)
src/lazr/restfulclient/resource.py (+34/-3)
src/lazr/restfulclient/tests/example.py (+5/-2)
To merge this branch: bzr merge lp:~leonardr/lazr.restfulclient/user-agent
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+23237@code.launchpad.net

Description of the change

This branch makes lazr.restfulclient send out a real User-Agent string instead of the python/httplib2 default. The User-Agent consists of a number of segments encased in parentheses, like this:

(launchpadlib 1.6.1) (lazr.restfulclient 1.0.0) (OAuth consumer=apport) (+cache-control)

The first segment is optional; it's information provided by an application that builds on lazr.restfulclient (ie. launchpadlib). The second segment is lazr.restfulclient version information. The third is also optional, and it's information provided by the authorization method (in this case, the OAuth consumer key). The fourth is also optional: it's a series of short strings advertising the client's support for particular features that lazr.restful can't serve to everyone because they break clients that don't support them.

That fourth section (+cache-control) is the reason I'm doing this branch in the first place. I want an easy way to distinguish clients that have a workaround for http://code.google.com/p/httplib2/issues/detail?id=97 from clients that don't. (lazr.restfulclient doesn't have this workaround in place right now, but it will before the next release.)

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Unless there's a good reason not to, I think that you should do parameterization the way it's done in other HTTP headers, by appending a semicolon, and a list of x=y pairs. (e.g. Content-Type: text/html; charset=ISO-8859-4).

I also suggest that since you only want to disable cache-control due to a bug in python's httplib2, you only disable it for the 'python/httplib2' user-agent, and omit the +cache-control value.

review: Needs Fixing
93. By Leonard Richardson

Response to feedback.

Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks, looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restfulclient/NEWS.txt'
2--- src/lazr/restfulclient/NEWS.txt 2010-03-24 14:41:40 +0000
3+++ src/lazr/restfulclient/NEWS.txt 2010-04-12 17:05:37 +0000
4@@ -2,10 +2,16 @@
5 NEWS for lazr.restfulclient
6 ===========================
7
8+0.9.14 (Development)
9+====================
10+
11+ - Clients now send a useful and somewhat customizable User-Agent
12+ string.
13+
14 0.9.13 (2010-03-24)
15 ===================
16
17-- Removed of some no-longer-needed compatibility code for buggy
18+- Removed some no-longer-needed compatibility code for buggy
19 servers, and fixed the tests to work with the new release of simplejson.
20
21 - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't
22
23=== modified file 'src/lazr/restfulclient/_browser.py'
24--- src/lazr/restfulclient/_browser.py 2010-03-24 14:29:00 +0000
25+++ src/lazr/restfulclient/_browser.py 2010-04-12 17:05:37 +0000
26@@ -225,7 +225,7 @@
27 """A class for making calls to lazr.restful web services."""
28
29 def __init__(self, service_root, credentials, cache=None, timeout=None,
30- proxy_info=None):
31+ proxy_info=None, user_agent=None):
32 """Initialize, possibly creating a cache.
33
34 If no cache is provided, a temporary directory will be used as
35@@ -239,6 +239,7 @@
36 cache = MultipleRepresentationCache(cache)
37 self._connection = service_root.httpFactory(
38 credentials, cache, timeout, proxy_info)
39+ self.user_agent = user_agent
40
41 def _request(self, url, data=None, method='GET',
42 media_type='application/json', extra_headers=None):
43@@ -251,6 +252,8 @@
44
45 # Add extra headers for the request.
46 headers = {'Accept' : media_type}
47+ if self.user_agent is not None:
48+ headers['User-Agent'] = self.user_agent
49 if isinstance(self._connection.cache, MultipleRepresentationCache):
50 self._connection.cache.request_media_type = media_type
51 if extra_headers is not None:
52
53=== modified file 'src/lazr/restfulclient/authorize/__init__.py'
54--- src/lazr/restfulclient/authorize/__init__.py 2009-10-06 18:23:19 +0000
55+++ src/lazr/restfulclient/authorize/__init__.py 2010-04-12 17:05:37 +0000
56@@ -55,6 +55,17 @@
57 """
58 pass
59
60+ @property
61+ def user_agent_params(self):
62+ """Any parameters necessary to identify this user agent.
63+
64+ By default this is an empty dict (because authentication
65+ details don't contain any information about the application
66+ making the request), but when a resource is protected by
67+ OAuth, the OAuth consumer name is part of the user agent.
68+ """
69+ return {}
70+
71
72 class BasicHttpAuthorizer(HttpAuthorizer):
73 """Handles authentication for services that use HTTP Basic Auth."""
74
75=== modified file 'src/lazr/restfulclient/authorize/oauth.py'
76--- src/lazr/restfulclient/authorize/oauth.py 2009-10-06 18:23:19 +0000
77+++ src/lazr/restfulclient/authorize/oauth.py 2010-04-12 17:05:37 +0000
78@@ -70,6 +70,16 @@
79 self.access_token = access_token
80 self.oauth_realm = oauth_realm
81
82+ @property
83+ def user_agent_params(self):
84+ """Any information necessary to identify this user agent.
85+
86+ In this case, the OAuth consumer name.
87+ """
88+ if self.consumer is None:
89+ return {}
90+ return {'oauth_consumer' : self.consumer.key}
91+
92 def load(self, readable_file):
93 """Load credentials from a file-like object.
94
95
96=== modified file 'src/lazr/restfulclient/docs/authorizer.txt'
97--- src/lazr/restfulclient/docs/authorizer.txt 2010-03-16 15:46:49 +0000
98+++ src/lazr/restfulclient/docs/authorizer.txt 2010-04-12 17:05:37 +0000
99@@ -81,11 +81,21 @@
100 ServiceRoot--its 'service root' resource doesn't match the WADL--but
101 we're able to make HTTP requests without getting 401 errors.
102
103+Note that the HTTP request includes the User-Agent header, but that
104+that header contains no special information about the authorization
105+method. This will change when the authorization method is OAuth.
106+
107+ >>> import httplib2
108+ >>> httplib2.debuglevel = 1
109+
110 >>> authorizer = BasicHttpAuthorizer("user", "password")
111 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
112+ send: 'GET / ...user-agent: lazr.restfulclient ...'
113+ ...
114
115 Teardown.
116
117+ >>> httplib2.debuglevel = 0
118 >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)
119
120
121@@ -175,17 +185,23 @@
122 ...
123
124 But valid credentials work fine (again, up to the point at which
125-lazr.restfulclient runs against the limits of this simple web service).
126+lazr.restfulclient runs against the limits of this simple web
127+service). Note that the User-Agent header mentions the name of the
128+OAuth consumer.
129
130+ >>> httplib2.debuglevel = 1
131 >>> authorizer = OAuthAuthorizer(
132 ... valid_consumer.key, access_token=valid_token)
133 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
134+ send: 'GET /...user-agent: lazr.restfulclient... oauth_consumer="consumer"...'
135+ ...
136+ >>> httplib2.debuglevel = 0
137
138 It's even possible to get anonymous access by providing an empty
139 access token.
140
141 >>> authorizer = OAuthAuthorizer(
142- ... valid_consumer, access_token=empty_token)
143+ ... valid_consumer.key, access_token=empty_token)
144 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
145
146 Because of the way the AnonymousAccessDataStore (defined
147
148=== modified file 'src/lazr/restfulclient/resource.py'
149--- src/lazr/restfulclient/resource.py 2010-02-10 14:25:19 +0000
150+++ src/lazr/restfulclient/resource.py 2010-04-12 17:05:37 +0000
151@@ -30,6 +30,7 @@
152
153
154 import cgi
155+from email.message import Message
156 import simplejson
157 from StringIO import StringIO
158 import urllib
159@@ -41,6 +42,8 @@
160 from _json import DatetimeJSONEncoder
161 from errors import HTTPError
162
163+from lazr.restfulclient import __version__
164+
165 missing = object()
166
167 class HeaderDictionary:
168@@ -380,7 +383,8 @@
169 RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}
170
171 def __init__(self, authorizer, service_root, cache=None,
172- timeout=None, proxy_info=None, version=None):
173+ timeout=None, proxy_info=None, version=None,
174+ base_client_name=''):
175 """Root access to a lazr.restful API.
176
177 :param credentials: The credentials used to access the service.
178@@ -394,9 +398,14 @@
179 if service_root[-1] != '/':
180 service_root += '/'
181 self._root_uri = URI(service_root)
182+
183+ # Set up data necessary to calculate the User-Agent header.
184+ self._base_client_name = base_client_name
185+
186 # Get the WADL definition.
187+ self.credentials = authorizer
188 self._browser = Browser(
189- self, authorizer, cache, timeout, proxy_info)
190+ self, authorizer, cache, timeout, proxy_info, self._user_agent)
191 self._wadl = self._browser.get_wadl_application(self._root_uri)
192
193 # Get the root resource.
194@@ -404,7 +413,29 @@
195 bound_root = root_resource.bind(
196 self._browser.get(root_resource), 'application/json')
197 super(ServiceRoot, self).__init__(None, bound_root)
198- self.credentials = authorizer
199+
200+ @property
201+ def _user_agent(self):
202+ """The value for the User-Agent header.
203+
204+ This will be something like:
205+ launchpadlib 1.6.1, lazr.restfulclient 1.0.0; oauth_consumer=apport
206+
207+ That is, a string describing lazr.restfulclient and an
208+ optional custom client built on top, and parameters
209+ containing any authorization-specific information that
210+ identifies the user agent (such as the OAuth consumer key).
211+ """
212+ base_portion = "lazr.restfulclient %s" % __version__
213+ if self._base_client_name != '':
214+ base_portion = self._base_client_name + ' (' + base_portion + ')'
215+
216+ message = Message()
217+ message['User-Agent'] = base_portion
218+ if self.credentials is not None:
219+ for key, value in self.credentials.user_agent_params.items():
220+ message.set_param(key, value, 'User-Agent')
221+ return message['User-Agent']
222
223 def httpFactory(self, authorizer, cache, timeout, proxy_info):
224 return RestfulHttp(authorizer, cache, timeout, proxy_info)
225
226=== modified file 'src/lazr/restfulclient/tests/example.py'
227--- src/lazr/restfulclient/tests/example.py 2010-02-10 14:21:45 +0000
228+++ src/lazr/restfulclient/tests/example.py 2010-04-12 17:05:37 +0000
229@@ -49,7 +49,10 @@
230 RESOURCE_TYPE_CLASSES['recipes'] = RecipeSet
231 RESOURCE_TYPE_CLASSES['cookbooks'] = CookbookSet
232
233- def __init__(self, service_root="http://cookbooks.dev/", version='1.0',
234- cache=None):
235+ DEFAULT_SERVICE_ROOT = "http://cookbooks.dev/"
236+ DEFAULT_VERSION = "1.0"
237+
238+ def __init__(self, service_root=DEFAULT_SERVICE_ROOT,
239+ version=DEFAULT_VERSION, cache=None):
240 super(CookbookWebServiceClient, self).__init__(
241 None, service_root, cache=cache, version=version)

Subscribers

People subscribed via source and target branches