Merge lp:~leonardr/launchpadlib/retry-on-invalid-token into lp:launchpadlib
| Status: | Merged |
|---|---|
| Merged at revision: | 102 |
| Proposed branch: | lp:~leonardr/launchpadlib/retry-on-invalid-token |
| Merge into: | lp:launchpadlib |
| Diff against target: |
1625 lines (+931/-238) 7 files modified
src/launchpadlib/NEWS.txt (+5/-1) src/launchpadlib/credentials.py (+234/-26) src/launchpadlib/launchpad.py (+228/-137) src/launchpadlib/testing/helpers.py (+57/-0) src/launchpadlib/tests/test_http.py (+228/-0) src/launchpadlib/tests/test_launchpad.py (+165/-74) src/launchpadlib/uris.py (+14/-0) |
| To merge this branch: | bzr merge lp:~leonardr/launchpadlib/retry-on-invalid-token |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Edwin Grubbs (community) | code | Approve on 2010-12-20 | |
| Benji York (community) | code* | 2010-12-15 | Approve on 2010-12-16 |
|
Review via email:
|
|||
Description of the Change
This branch moves almost all of the code having to do with OAuth tokens--getting the request token, exchanging it for an access token, storing it in the keyring and retrieving it from the keyring--into the RequestTokenAut
Why did I do this? Because of bug 670886. Up to this point, we've been assuming that once you exchange a request token for an access token, or retrieve an access token from local store, you can make HTTP requests to the web service and never have to think about tokens anymore. But that's not true. Since you last used that cached token, it might have expired, or you might have cancelled it. Worse, it might expire thirty minutes into your launchpadlib session. When that happens, launchpadlib simply crashes.
To fix this bug, I had to take the mechanism used to get new access tokens and make it portable, Make it so all the information about an access token--the application name, the allowable access levels, whether to store the access token in the keyring or somewhere else, whether to authorize the request token using a web browser open or some other mechanism--was all kept in a single object, which could be stored with the Launchpad instance and brought out of retirement if Launchpad ever found itself using an expired or invalid token.
The main non-refactoring code is the new LaunchpadOAuthA
I got all the existing tests to work and added several new tests to clarify the behavior of Launchpad.
Problems with the branch, which I'm working on:
1. I haven't checked that the launchpadlib integration tests within Launchpad still work.
2. I don't have a test for the new behavior itself. We can't test Launchpad's HTTP behavior in general--that has to be done manually or within Launchpad--but I'm thinking of writing a fake Http subclass that just gives out 401 errors. That should be enough to write a test.
3. The code that checks the 401 is a little brittle: it looks at the entity-body to distinguish between a 401 caused by a bad token and a 401 caused by accessing data you don't have access to. We might decide to change or localize this one day. I'd like to do a Launchpad branch that puts this information in an X- HTTP header, and do a follow-up launchpadlib branch that looks at the HTTP header instead of the entity-body.
(I'm not sure whether "private data" gives you a 401 or 403. If that's a 403, it should make sense to get a new token every time a signed request yields a 401. That would simplify the code.)
Since this is a rather large branch I'm happy to write a guide to the code--just let me know.
| Benji York (benji) wrote : | # |
| Benji York (benji) wrote : | # |
Here's the rest of my review:
src/launchpadli
I know people most often use .login_with() to create instances, do they
ever use the constructor?
If so and they are passing service_root as a positional argument, then
insertion of the authorization_
cause their code to generate an error.
src/launchpadli
allow_
src/launchpadli
Since max_failed_attempts is ignored, maybe None would be a better default
value.
src/launchpadli
This note on the allow_access_levels parameter
This argument is used to construct the default `authorization_
so if you pass in your own `authorization_
argument will be ignored. This argument will also be ignored unless
you also specify `consumer_name`.
...is sufficiently complex that I think programmatic enforcement of the
given rules is warranted. Unfortunately for backward compatibility
reasons I suspect we con only enforce the first rule (passing
authorizati
src/launchpadli
A line over 78 characters wide.
src/launchpadli
A line over 78 characters wide.
None of these are major, and I'm sure they'll all be addressed forthwith (even if some don't spur changes), so approving.
Since I'm doing mentored reviews, Edwin will have to review my review before final approval. I'll let him know to look at this review once you're response is in.
- 114. By Leonard Richardson on 2010-12-16
-
Added tests that require simulating HTTP responses.
- 115. By Leonard Richardson on 2010-12-16
-
Basic tests of the mock-HTTP-response code itself.
- 116. By Leonard Richardson on 2010-12-16
-
Track how many access tokens were obtained, not whether some have been obtained.
| Leonard Richardson (leonardr) wrote : | # |
Here's an incremental diff: http://
I've written a framework that lets me send fake HTTP responses to launchpadlib, allowing me to finally do a (sort of) end-to-end test of login_with(), as well as demonstrate what happens if Launchpad suddenly starts sending 401 responses to launchpadlib's requests. I had to do a tiny bit of refactoring of the LaunchpadOauthA
| Benji York (benji) wrote : | # |
On Thu, Dec 16, 2010 at 1:17 PM, Leonard Richardson
<email address hidden> wrote:
> Here's an incremental diff: http://
I don't see any problems introduced in the diff.
--
Benji York
- 117. By Leonard Richardson on 2010-12-16
-
Response to feedback, focusing on docstrings and default arguments.
- 118. By Leonard Richardson on 2010-12-16
-
Added checks for incompatible information passed into login_with. Made application_name optional, but started enforcing the precense of *some* way of identifying the application.
- 119. By Leonard Richardson on 2010-12-16
-
Application name and consumer name are not the same thing, so don't allow the creation of an authorization engine that specifies the same value for both.
- 120. By Leonard Richardson on 2010-12-16
-
Minor cleanup.
- 121. By Leonard Richardson on 2010-12-20
-
Merge with trunk.
| Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Leonard,
This is a nice branch, and I think benji did an excellent review,
however, since the branch is so big, I found a few more items to improve.
-Edwin
>=== modified file 'src/launchpadl
>--- src/launchpadli
>+++ src/launchpadli
>@@ -124,6 +107,40 @@
> collection_of = 'distribution'
>
>
>+class LaunchpadOAuthA
>+ """Detects expired/invalid OAuth tokens and tries to get a new token."""
>+
>+ def __init__(self, launchpad, authorization_
>+ self.launchpad = launchpad
>+ self.authorizat
>+ super(Launchpad
>+
>+ def _bad_oauth_
>+ """Helper method to detect an error caused by a bad OAuth token."""
>+ return (response.status == 401 and
>+ (content.
>+ or content.
>+
>+ def _request(self, *args):
>+ response, content = super(
>+ LaunchpadOAuthA
>+ return self.retry_
If there is a bug that causes continual 401 errors, it will
eventually reach the max recursion depth and give a ginormous traceback.
It seems like each request should only need to be retried once or twice
before raising an exception.
>+
>+ def retry_on_
>+ """If the response indicates a bad token, get a new token and retry.
>+
>+ Otherwise, just return the response.
>+ """
>+ if (self._
>+ and self.authorizat
>+ # This access token is bad. Scrap it and create a new one.
>+ self.launchpad.
>+ self.authorizat
>+ # Retry the request with the new credentials.
>+ return self._request(
>+ return response, content
>+
>+
> class Launchpad(
> """Root Launchpad API class.
>
>=== added file 'src/launchpadl
>--- src/launchpadli
>+++ src/launchpadli
>+class TestAbilityToPa
>+ """Test launchpadlib's ability to handle the sample data.
>+
>+ To create a Launchpad object, two HTTP requests must succeed and
>+ return usable data: the requests for the WADL and JSON
>+ representations of the service root. This test shows that the
>+ minimal data in SIMPLE_WADL and SIMPLE_JSON is good enough to
>+ create a Launchpad object.
>+ """
>+
>+ def test_minimal_
>+ """Make sure that launchpadlib can use the minimal data."""
>+ launchpad = self.launchpad_
>+ Response(200, SIMPLE_WADL),
>+ Response(200, SIMPLE_JSON))
>+
>+ def test_bad_
>+ """Show that bad WADL causes an exception."""
>+ sel...
- 122. By Leonard Richardson on 2010-12-20
-
Split out tests at Edwin's request.
| Leonard Richardson (leonardr) wrote : | # |
> >+ def _request(self, *args):
> >+ response, content = super(
> >+ LaunchpadOAuthA
> >+ return self.retry_
>
>
>
> If there is a bug that causes continual 401 errors, it will
> eventually reach the max recursion depth and give a ginormous traceback.
> It seems like each request should only need to be retried once or twice
> before raising an exception.
Long before this happens, the user will get tired of browser opens and hit Ctrl-C. I looked into making this change, but I couldn't find a way to do it since I can't really add arguments to _request (it's an httplib2 method). Given the low impact of the change, I'm going to leave it out. I've made the other changes you suggest.

I've read through the entire diff and feel like I have a solid
understanding of the changes being made. The architectural
transformation not only accomplishes the desired effect, but makes the
responsibilities of the code better organized.
I have a few questions about particular bits of the code below. I have
run out of time today so I'm posting what I have and will finish first
thing tomorrow.
src/launchpadli b/credentials. py line 29:
I was surprised to see "import" and "from...import" lines intermixed (I
expected to see "from" first and then "import"). I don't see anything in
the style guide about them though, so this may be the preferred
style.
src/launchpadli b/credentials. py line 215:
The RequestTokenAut horizationEngin e interface has changed in what appear incompatible ways. Since this class is exported in
to be backward-
__all__, I assume it is a public interface that others may depend on. Is
this the case?
The same goes for AuthorizeReques tTokenWithBrows er and other subclasses.
src/launchpadli b/credentials. py line 231:
Mutable default arguments ("allow_ access_ levels= []") are a bug magnet. I
suggest using an empty tuple instead.
src/launchpadli b/credentials. py line 231:
Is there a reason why some of the __init__ parameters are documented but
others (like consumer_name) are not?
src/launchpadli b/credentials. py line 257:
Should it be an error to provide a value for allow_access_levels and not a access_ levels value to be discarded.
value for consumer_name because that will result in the caller-provided
allow_
src/launchpadli b/credentials. py line 264:
Similar to the above, should it be an error to provide a value for _name and value for consumer_name because that will result in
application
the caller-provided application_name value to be discarded.
src/launchpadli b/credentials. py line 259:
The "consumer_name = consumer.key" line seems to be dead code, the value
of consumer_name isn't used after that line.
src/launchpadli b/credentials. py line 343:
Minor: The new store_credentia ls_locally( ) and credentials_ from_local_ store() method names might could be more credentials_ to_local_ store" (I have to
retrieve_
symmetric. Perhaps "store_
admit that I don't like that name much either).
src/launchpadli b/credentials. py line 389:
It isn't part of your changes, but the WAITING_FOR_USER definition is one
very long line.