crafting [better] api clients

96
Crafting [Better] API Clients PyTennesee 2015- Ben Lopatin

Upload: wellfire-interactive

Post on 16-Jul-2015

54 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: Crafting [Better] API Clients

Crafting [Better] API Clients

PyTennesee 2015- Ben Lopatin

Page 2: Crafting [Better] API Clients

Partner and developer @ Wellfire Interactive

@bennylope

Page 3: Crafting [Better] API Clients
Page 4: Crafting [Better] API Clients

Clients, not Services

Page 5: Crafting [Better] API Clients

Why?

Page 6: Crafting [Better] API Clients

A great API is necessary but insufficient to be

useful.

Page 7: Crafting [Better] API Clients

This is the right way.

Page 8: Crafting [Better] API Clients

This is the right way. This is some good ideas.

Page 9: Crafting [Better] API Clients

Acknowledge that you’re making design decisions.

Page 10: Crafting [Better] API Clients

Some assumptions

Page 11: Crafting [Better] API Clients

Data Model

Page 12: Crafting [Better] API Clients

Why?

Page 13: Crafting [Better] API Clients

Database API

Real world

Page 14: Crafting [Better] API Clients

Layers of abstraction!

Page 15: Crafting [Better] API Clients

Connections & data

Page 16: Crafting [Better] API Clients

Designing the Python data interface

Page 17: Crafting [Better] API Clients

Explicit data attributes or implicit collection?

Page 18: Crafting [Better] API Clients

class DataResource: def __init__(self, id, first_name, last_name): self.id = id self.first_name = first_name self.last_name = last_name resource.first_name

Page 19: Crafting [Better] API Clients

resource = { 'id': 1, 'first_name': 'Ben', 'last_name': 'Lopatin', } resource['first_name']

Page 20: Crafting [Better] API Clients

class DataResource(dict): def update(self, client): # Do something cool here client.update(self.id, self.first_name, self.last_name)

Page 21: Crafting [Better] API Clients

Data or Resources?

• If you think about everything as a resource, then shouldn’t it have the same methods?

• Or is it data and the API is just providing some

• You’re not obliged to represent 1:1

Page 22: Crafting [Better] API Clients

Be mindful of what you discard

Page 23: Crafting [Better] API Clients

Second, we wanted to give you a heads up that we're announcing a new feature soon -- additional fields. This will allow people to get congressional districts, state legislative districts, timezones, and school districts with a forward or reverse lookup. We are looking to add more additional fields in the future (namely Census data).

Page 24: Crafting [Better] API Clients

Errors

Page 25: Crafting [Better] API Clients
Page 26: Crafting [Better] API Clients

Why?

Page 27: Crafting [Better] API Clients

What?

Page 28: Crafting [Better] API Clients

API application errors Authentication errors

Account errors …

Page 29: Crafting [Better] API Clients

If only there were a concept to encapsulate

errors in Python…

Page 30: Crafting [Better] API Clients

…that would be exceptional!

Page 31: Crafting [Better] API Clients

Exceptions for obvious flow control

Page 32: Crafting [Better] API Clients

try: response = client.request()except APIPaymentError: # Email corporate accounts payable raiseexcept APIAuthError: # Redirect to account settings raise

Page 33: Crafting [Better] API Clients

Map exceptions to API errors

Page 34: Crafting [Better] API Clients
Page 35: Crafting [Better] API Clients

class SmartyStreetsError(Exception): """Unknown SmartyStreets error""" def __str__(self): return self.__doc__class SmartyStreetsInputError(SmartyStreetsError): """HTTP 400 Bad input. Required fields missing from input or are malformed."""class SmartyStreetsAuthError(SmartyStreetsError): """HTTP 401 Unauthorized. Authentication failure; invalid credentials"""class SmartyStreetsPaymentError(SmartyStreetsError): """HTTP 402 Payment required. No active subscription found."""class SmartyStreetsServerError(SmartyStreetsError): """HTTP 500 Internal server error. General service failure; retry request."""

Page 36: Crafting [Better] API Clients

ERROR_CODES = { 400: SmartyStreetsInputError, 401: SmartyStreetsAuthError, 402: SmartyStreetsPaymentError, 500: SmartyStreetsServerError,}

Page 37: Crafting [Better] API Clients

…maybe not all errors

Page 38: Crafting [Better] API Clients
Page 39: Crafting [Better] API Clients

Errors aren’t necessarily limited to HTTP status

codes

Page 40: Crafting [Better] API Clients

Exceptions +

Error codes

Page 41: Crafting [Better] API Clients

Where?

Page 42: Crafting [Better] API Clients

Logging

Page 43: Crafting [Better] API Clients

Why?

Page 44: Crafting [Better] API Clients

What?

Page 45: Crafting [Better] API Clients

Verify requests issued

Page 46: Crafting [Better] API Clients

Request/response count

Page 47: Crafting [Better] API Clients

API performance

Page 48: Crafting [Better] API Clients

API errors

Page 49: Crafting [Better] API Clients

How?

Page 50: Crafting [Better] API Clients

@decorators?

Page 51: Crafting [Better] API Clients

@log_requestsdef _req(self, method='get', verb=None, headers={}, params={}, data={}): url = self.BASE_URL.format(verb=verb) request_headers = {'content-type': 'application/json'} request_params = {'api_key': self.API_KEY} request_headers.update(headers) request_params.update(params) return getattr(requests, method)(url, params=request_params, headers=request_headers, data=data)

Page 52: Crafting [Better] API Clients

def log_requests(func): def decorator(*args, **kwargs): request_id = str(uuid.uuid4()) logger.info("Requesting %s" % request_id) try: resp = func(*args, **kwargs) except: logger.exception("Request error %s" % request_id) raise logger.info("Response %s" % request_id) return resp return decorator

Page 53: Crafting [Better] API Clients

Testing

Page 54: Crafting [Better] API Clients

Why?

Page 55: Crafting [Better] API Clients
Page 56: Crafting [Better] API Clients

How?

Page 57: Crafting [Better] API Clients

Live API Mock servers

Mocked responses

Page 58: Crafting [Better] API Clients
Page 59: Crafting [Better] API Clients

That’s not nice (or fun)

Page 60: Crafting [Better] API Clients

Mock server?

Page 61: Crafting [Better] API Clients

Live testing’s hard, let’s go mocking!

Page 62: Crafting [Better] API Clients

Mocking requests

Page 63: Crafting [Better] API Clients

responses HTTPretty

Page 64: Crafting [Better] API Clients

responses HTTPretty betamax

Page 65: Crafting [Better] API Clients

@responses.activate def test_auth_error(self): responses.add(responses.POST, 'https://api.smartystreets.com/street-address', body='', status=401, content_type='application/json') self.assertRaises(SmartyStreetsAuthError, self.client.street_addresses, [{}, {}])

@responses.activate def test_payment_error(self): responses.add(responses.POST, 'https://api.smartystreets.com/street-address', body='', status=402, content_type='application/json') self.assertRaises(SmartyStreetsPaymentError, self.client.street_addresses, [{}, {}])

Page 66: Crafting [Better] API Clients

@httpretty.activate def test_auth_error(self): """Ensure an HTTP 403 code raises GeocodioAuthError""" httpretty.register_uri(httpretty.GET, “http://api.geocod.io/v1/parse", body="This does not matter", status=403) self.assertRaises(GeocodioAuthError, self.client.parse, "")

Page 67: Crafting [Better] API Clients

There can be benefits to some live testing

Page 68: Crafting [Better] API Clients
Page 69: Crafting [Better] API Clients

Performance

Page 70: Crafting [Better] API Clients

Connections

Page 71: Crafting [Better] API Clients

Batching

Page 72: Crafting [Better] API Clients

The right API methods

Page 73: Crafting [Better] API Clients

Data structures

Page 74: Crafting [Better] API Clients

class APIData: def init(self, id, name, image): self.id = id self.name = name self.image = image

Page 75: Crafting [Better] API Clients

class APIData: __slots__ = ['id', 'name', 'image'] def init(self, id, name, image): self.id = id self.name = name self.image = image

Page 76: Crafting [Better] API Clients

Concurrency

Page 77: Crafting [Better] API Clients

response_iter = ( grequests.post( url=url, data=json.dumps(data_chunk), params=params, headeresponse_iter=headeresponse_iter, ) for data_chunk in chunker(data, 100) ) responses = grequests.imap(response_iter, size=parallelism)status_codes = {}addresses = AddressCollection([])for response in responses: if response.status_code not in status_codes.keys(): status_codes[response.status_code] = 1 else: status_codes[response.status_code] += 1 if response.status_code == 200: addresses[0:0] = AddressCollection(response.json())

Page 78: Crafting [Better] API Clients

session = FuturesSession(max_workers=parallelism)session.headers = headerssession.params = paramsfutures = [ session.post(url, data=json.dumps(data_chunk)) for data_chunk in chunker(data, 100) ] while not all([f.done() for f in futures]): continuestatus_codes = {}responses = [f.result() for f in futures]addresses = AddressCollection([])for response in responses: if response.status_code not in status_codes.keys(): status_codes[response.status_code] = 1 else: status_codes[response.status_code] += 1 if response.status_code == 200: addresses[0:0] = AddressCollection(response.json())

Page 79: Crafting [Better] API Clients

You know Twisted can do that, right?

Page 80: Crafting [Better] API Clients

Security

Page 81: Crafting [Better] API Clients

HTTPS

Page 82: Crafting [Better] API Clients

Python 2.7

• pyOpenSSL

• ndg-httpsclient

• pyasn1

Page 83: Crafting [Better] API Clients

Authenticating

Page 84: Crafting [Better] API Clients

Docs

Page 85: Crafting [Better] API Clients

Django

Page 86: Crafting [Better] API Clients

No?

Page 87: Crafting [Better] API Clients

Python library

Anything Django related

Page 88: Crafting [Better] API Clients

• Create the client *first* then Django integration is a bonus.

• Mapping models to a distant API.

• It’ll be easier for you to maintain and test

• Easier for other people to use for other things

Page 89: Crafting [Better] API Clients

class SomeModel(models.Model): my_field = models.CharField(max_length=100) def sync(self, client=None): if client is None: client = APIClient() client.update(id=self.id, name=self.my_field)

Page 90: Crafting [Better] API Clients

Code Gen

Page 91: Crafting [Better] API Clients

Starting from (DSL) API documentation, automatically

generate matching client code

Page 92: Crafting [Better] API Clients
Page 93: Crafting [Better] API Clients

Crank ‘em out

Page 94: Crafting [Better] API Clients

In [hypothetical] practice?

Page 95: Crafting [Better] API Clients

Suitability?

Page 96: Crafting [Better] API Clients

The End