Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Allow access to request metadata on success #371

Merged
merged 2 commits into from
Dec 22, 2017

Conversation

sedouard
Copy link
Contributor

Hey @ob-stripe this is my first stab at getting response headers out of the python bindings.

I'm trying to follow the same pattern by including a response resource to the returned API models.

However one tricky part is understanding where the best spot is to place an optional flag to include the response field on returned API resources. Each API request method contains a paramsparameter that directly maps to the API's URL parameters. Placing some kind of include_response flag here would stick out since it won't be going to the API.

Another thought was using a static variable somewhere and checking that variable within the api_requestor to include the response or not.

Do you have any opinions here?

@ob-stripe
Copy link
Contributor

sigh Yeah. The way we handle keyword parameters in Python makes it really inconvenient to add new "special" parameters. We already have a few of those (api_key, stripe_version, stripe_account, idempotency_key) and they need to be present on every request method's signature (e.g. here).

I would really rather avoid introducing new special parameters, esp. since this one as you pointed out would just be a configuration flag not directly related to the API. How about introducing a context manager for this? E.g.:

with stripe.catch_responses() as r:
    charge = stripe.Charge.retrieve("ch_...")
    request_id = r[0].headers['Request-Id']

Basically, the context manager would collect the responses to all requests sent within the context.

@sedouard
Copy link
Contributor Author

My concern with that approach is that to get your headers for any arbitrary response the consuming application needs to keep track of sequence number of the request which sounds like a pain. But then again I don't regularly develop asynchronous code in python - so maybe it is not a big deal.

I personally think some type of options hash on the base stripe import would be more straight forward however the downside is that it would be a global change to the application and not on a request-by-request basis. Is that something we'd be ok with?

Both of our approaches in java PR and in the the Node SDK issue intend on using some reference off the retrieved api model object. I was hoping we could do something similar here

@dalan-stripe do you have any opinions about the UX regarding how to optionally enable responses upon requests?

@ob-stripe
Copy link
Contributor

ob-stripe commented Nov 10, 2017

What if the context manager attached the response object to the resource object?

with stripe.store_responses():
    charge = stripe.Charge.retrieve("ch_...")
    request_id = charge.response.headers['Request-Id']

(Just thinking aloud here, not sure how feasible this would be in practice.)

@sedouard
Copy link
Contributor Author

@ob-stripe Yeah I like that much better. I think the name should be include_responses() such that it conforms more to the naming of java's optional flag and it doesn't imply that stripe will hold references to each response made through the library.

This way consumers can manage the scope of the optional change. I'm actually good with this approach

@ob-stripe
Copy link
Contributor

👍

You might want to take a look at the implementation in stripe-ruby. Basically the HTTP client holds a reference to the last response (here), and the request method returns the reference in addition to the API resource (here).

In our case we would do something similar, except that we'd copy the last response object onto the resource's response attribute.

@dalan-stripe
Copy link

I'm not so sure that this would be an appropriate usage of a context manager in Python. It also adds a lot of complexity to python-stripe I believe, because wouldn't you need to build your request objects in some way and then do?:

stripe.bulk_requests = True #signals that requests will be done in bulk 
requests = [stripe.Charge.retrieve('ch_123'), stripe.Charge.retrieve('ch_124'), stripe.Charge.retrieve('ch_125')] 

with requests.include_responses() as completed_requests: 
  for response,  headers in completed_requests: 
    print(f"{response} - {headers}")

It just seems very un-pythonic and potentially introduces problems for async (not certain if we support this anyway)

My vote is to just include it by default in the spirit of simplicity on the outermost requested resource. For example .headers exists on the Charge if you only did .retrieve but exists on the list if you did a stripe.Charge.list(). It seems like given the exposure to Requests in Python you'd just expect .headers to exist on the object itself. Although I could see the confusion between having this object that represents a resource on Stripe that is also munged together with an HTTP request.

@ob-stripe
Copy link
Contributor

After giving it some more thought, I agree that a context manager may not be the best solution here. Also, we don't persist clients in the Python library (which is probably something we should fix too) which makes my proposed solution somewhat difficult to implement.

I don't think we should start storing responses by default however, so as not to increase the memory footprint for users who don't need this feature. Also, we should not expose any requests-specific attributes as the library can use different HTTP clients.

How about this:

  • have a global configuration flag that needs to be enabled, e.g. Stripe.include_responses = True

  • when the flag is enabled, all Stripe resources returned by the API have a response attribute, which is an instance of StripeResponse

  • StripeResponse stores the body, headers and status code of the response

We'll also probably have to define a custom JSON encoder to exclude response from the serialization of Stripe objects.

@deontologician-stripe
Copy link
Contributor

Why do we want to optionally make the headers available? Why not just make them always available?

@sedouard
Copy link
Contributor Author

@deontologician-stripe I think the concern is that if there's a consumer that happens to be making a ton of Stripe requests (and holding references to each response) that adding a reference to each of those response objects we deserialize and return to the consumer could cause memory bloat.

@ob-stripe can elaborate if I'm off.

@deontologician-stripe
Copy link
Contributor

As long as you're not holding references to all of your objects, it shouldn't be too much bloat. I think this is probably a case of premature optimization. In any case I agree with @dalan-stripe that having a context manager just for this option is not great ergonomics. A global module flag would be a good option, but ultimately I don't think this feature warrants a flag to turn it off since it's not going to cause most people any problems

@ob-stripe
Copy link
Contributor

So, it turns out that this feature has already existed in stripe-php for a little over 2 years: stripe/stripe-php#206.

In light of the arguments above and in the interest of feature parity and implementation consistency across our libraries, I recommend that:

  • we store the last response object on all API resource instances (by default and with no configuration flag)
  • we store the attribute in _last_response and make it accessible via get_last_response() (StripeObjects handle non-_-prefixed attributes as normal dictionary keys, which would cause a bunch of issues in this case)
  • we blacklist last_response as a field name in the API

We'd also need to update the pending Java implementation accordingly.

@sedouard sedouard force-pushed the success-response-headers branch from d9f3976 to 63a4916 Compare November 15, 2017 21:23
@sedouard
Copy link
Contributor Author

sedouard commented Nov 15, 2017

@ob-stripe sounds good. I've updated the PR to go in this direction.

Only change is a little different is that I don't think get_last_response() is very pythonic. Instead I went with using an property for StripeObject.last_response pointing to StripeObject._last_response. It doesn't appear this is being serialized out on future instance method calls.

@sedouard sedouard changed the title WIP - Allow access to request metadata on success Allow access to request metadata on success Nov 15, 2017
@ob-stripe
Copy link
Contributor

ob-stripe commented Nov 17, 2017

Only change is a little different is that I don't think get_last_response() is very pythonic. Instead I went with using an property for StripeObject.last_response pointing to StripeObject._last_response. It doesn't appear this is being serialized out on future instance method calls.

Nice! Good call. 👍

I'm not a big fan of storing the StripeResponse instance inside the resp dictionary however. I find it fairly confusing to have a dictionary that contains a _last_response key whose value is a StripeResponse, that has a body attribute whose value is the JSON string of the top-level dictionary, minus the _last_response key.

Instead, I think StripeResponse should contain both the deserialized dictionary and the JSON string, and request() should return a StripeResponse instead of a dictionary. This will require modifying a few places to look for the now nested dictionary (e.g. call convert_to_stripe_object() with resp.data instead of resp).

This would also bring the implementation closer to stripe-ruby's:

wdyt?

@sedouard
Copy link
Contributor Author

sedouard commented Nov 18, 2017

I'm not a big fan of storing the StripeResponse instance inside the resp dictionary however. I find it fairly confusing to have a dictionary that contains a _last_response key whose value is a StripeResponse, that has a body attribute whose value is the JSON string of the top-level dictionary, minus the _last_response key.

fair I think the modifications in the unit test reflect this awkwardness.

I think the reason I did it this way was to get around having to make a bunch of changes all over the API resource classes which use the requestor interface directly :-/

# the raw API response information
stripe_response = None

if isinstance(resp, stripe.stripe_response.StripeResponse):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ob-stripe I'm able to reduce the amount of code change needed from changing APIRequestor.request by doing this little awkward bit. Essentially unpacking the response body dict inside convert_to_stripe_object.

APIRequestor.request's only real consumer is convert_to_stripe_object we only need to change it here.

wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Yeah, that sounds reasonable and less annoying than having to change all the call sites. It also maintains the existing behavior of convert_to_stripe_object which is nice even if it's an internal function.

@sedouard
Copy link
Contributor Author

sedouard commented Nov 20, 2017

@ob-stripe I updated the PR with a similar approach to what you mention. The exception is instead of calling convert_to_stripe_object with a resp.data arg, I'm unpacking that arg within the function to prevent having to change a bunch of calling code to convert_to_stripe_object

@sedouard sedouard force-pushed the success-response-headers branch from b423683 to c96d954 Compare November 20, 2017 23:58
Copy link
Contributor

@ob-stripe ob-stripe left a comment

Choose a reason for hiding this comment

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

Requested one small change, but I think we'll be ready to merge afterwards. Thanks for bearing with me :)

@@ -357,7 +358,8 @@ def interpret_response(self, rbody, rcode, rheaders):
rbody, rcode, rheaders)
if not (200 <= rcode < 300):
self.handle_error_response(rbody, rcode, resp, rheaders)
return resp

return StripeResponse(rbody, rcode, rheaders)
Copy link
Contributor

Choose a reason for hiding this comment

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

One minor optimization is that you could instantiate the StripeResponse in the try clause, to avoid parsing the JSON twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ done!

# the raw API response information
stripe_response = None

if isinstance(resp, stripe.stripe_response.StripeResponse):
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Yeah, that sounds reasonable and less annoying than having to change all the call sites. It also maintains the existing behavior of convert_to_stripe_object which is nice even if it's an internal function.

@sedouard sedouard force-pushed the success-response-headers branch from 5177762 to adf3fb3 Compare December 4, 2017 22:53
@ob-stripe
Copy link
Contributor

Okay, this looks good to me! Thanks for making all the changes @sedouard.

@brandur-stripe, mind taking a quick look as well?

@brandur-stripe
Copy link
Contributor

Sorry for the delay guys! And nice one @sedouard, this looks great. Huge +1 on building out a custom StripeResponse class instead of relying on some kind of loosely typed dict.

This is a bit of a nit, but do you think we could introduce a basic unit test suite for StripeResponse? It doesn't have to do that much, but maybe just make sure of things like (1) it parses JSON in body into data, (2) passes idempotency_key and request_id through to the headers dict. It may be a little redundant right now, but I think it'll be helpful in the future if anyone adds something a little more complicated to StripeResponse because the groundwork will be lain for them to start writing tests off of.

Otherwise, LGTM. Thanks again.

@jleclanche
Copy link
Contributor

Paging devs, is this getting merged?

@brandur-stripe
Copy link
Contributor

Yeah, we should get this in.

@sedouard Mind taking a look at the comment above? Thanks!

@stevene-stripe
Copy link

Yeah sorry guys for letting this PR hang out! I'll get this buttoned up in the next couple days. I got caught up with some other hi-pri work.

@stevene-stripe
Copy link

@brandur-stripe I added new tests for StripeResponse covering all methods and fields.

@brandur-stripe
Copy link
Contributor

Looks great! Thanks @stevene-stripe!

@brandur-stripe brandur-stripe merged commit 2f719d6 into stripe:master Dec 22, 2017
@brandur-stripe
Copy link
Contributor

Released as 1.77.0.

@jleclanche
Copy link
Contributor

Thanks! :)

@jleclanche
Copy link
Contributor

jleclanche commented Feb 6, 2018

Hey, am I crazy or should this work?

customer = stripe_customer.api_retrieve()
customer.account_balance -= 250
customer.save(idempotency_key=ik)
credit_request_id = customer.last_response.request_id  # Breaks! last_response is None

@ob-stripe
Copy link
Contributor

@jleclanche You're not crazy! Looks like the current implementation does not work well with save() calls.

Mind opening a new issue for this? I think the fix should be fairly simple.

@jleclanche
Copy link
Contributor

Sure, #393

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants