Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a REST API with Hug

Building a REST API with Hug

I show the current features and niceties of Hug and then I present the design patterns we use for a production API.

Avatar for Alexandre M. S.

Alexandre M. S.

July 01, 2018

Other Decks in Technology

Transcript

  1. Who am I? Alexandre Savio Engineer @ Siemens (Software De

    ned Intervert, Energy) Part time postdoc researcher @ TUM (http://neurita.github.io/) Python conference organizer EuroPython Society Board PySS, San Sebastián, Spain PyMunich/PyconWeb 2
  2. Hug Embrace the APIs of the future Currently in version

    2.4.0. Made for Python3. Built on top of Falcon. https://falconframework.org/  github.com/timothycrosley/hug 4
  3. routing with function decorators import hug def root(): return 'Welcome

    home!' router = hug.route.API(__name__) router.get('/home')(root) 7
  4. types for validation or data transformation import hug @hug.get() def

    weird_salute(first_name: hug.types.text, age: int): if age > 80: return f"Hi {first_name}, you look so young today!" return f"Hi {first_name}!" Many built-in options Custom types Marshmallow integration 8
  5. directives to access API metadata @hug.get() def root(session: hug.directives.session): session

    # is here automatically, without needing to be passed in Built-in: timer , module , api_version , api , documentation , session , user ... Custom directives, e.g., client_type 9
  6. Other features Output formatting @hug.get(output=hug.output_format.html) def home(): return # HTML

    generating code goes here Command-line interface Built-in API versioning 3rd-party plugins Testing 10
  7. What else do we need? Marshmallow (https://marshmallow.readthedocs.io/) SQLAlchemy (http://www.sqlalchemy.org/) Alembic

    (http://alembic.zzzcomputing.com/en/latest/) Factory Boy (https://factoryboy.readthedocs.io/) PyTest (http://pytest.org/) 13
  8. # books/api/resources/books/controller.py import hug from books.api.router import api from books.api.router.interceptor

    import authentication from books.exceptions import RecordNotFound from books.services.book import DeviceService @api.urls('/books') class BooksController(): @api.get('', requires=[authentication(),]) def index(self, user: hug.directives.user): """Return the list of available books""" books = BooksService.where(user_id=user.id).all() return IndexPresenter(books) ... 17
  9. # books/api/resources/books/controller.py ... @api.get('{book_id}', requires=[authentication(),]) def find(self, book_id: hug.types.uuid, user:

    hug.directives.user): """Return a book matching the id""" book = BooksService.find(book_id) if not book: raise RecordNotFound('Resource not found') if BooksService.is_my_book(book.id, user.id): return OwnedBookPresenter(book) return BookPresenter(book) 18
  10. from books.api.resources.base.presenter import BasePresenter from books.models import BaseModel class BookPresenter(BasePresenter):

    def transform(self, entry: BaseModel): return entry.pick(self.filter('id', 'title', 'description')) 21
  11. # books/api/router/interceptor/authentication.py ... def _authenticate(token: str) -> Union[None, bool, models.User]:

    """ Verifies the Token and return a proper response. """ try: claims = _jwt_verify(token) return _fetch_user(claims) if claims else None except Exception as e: LOG.debug('JWT verification failed: {}'.format(e)) return False def authentication() -> Callable: return hug.authentication.token(_authenticate) ... 24
  12. # books/api/router/exceptions/handler.py import hug @hug.exception(ResourceNotFound, exclude=hug.HTTPError) def handle_resource_not_found(exception): raise hug.HTTPError(

    status=hug.HTTP_NOT_FOUND, title='Resource not found', description=str(exception) ) @hug.exception(Exception, exclude=hug.HTTPError) def handle_unexpected_exception(exception): raise hug.HTTPError( status=hug.HTTP_INTERNAL_SERVER_ERROR, title='Internal Server Error', description=str(exception) ) 26
  13. # books/api/run.py import hug, books api = hug.API(__name__) @hug.extend_api(api=api) def

    extend_with_endpoints() -> tuple: return (books.api.resources.books.controller.BooksController, ) ... 28
  14. ... @hug.extend_api(api=api) def extend_with_error_handlers() -> tuple: from books.api.router.exceptions import handler

    return (handler, ) @hug.extend_api(api=api) def extend_with_transformer() -> tuple: from books.api.router.transformer import presenter return (presenter, ) if __name__ == '__main__': api.http.serve() 29
  15. # books/api/conftest.py import pytest from books.api.run import api from books.api.tests.base

    import crud_stub_controller class _APIProvider: """ Base cactus test class to provide API setup and teardown. """ @classmethod def setup(cls): api.extend(crud_stub_controller) @classmethod def teardown(cls): "Remove endpoints that start with '/test' in api.http.routes." 31
  16. # books/api/resources/books/tests/test_index.py import hug import pytest from books.models.factories.book import BookFactory

    @pytest.mark.usefixtures('api', 'context') class TestBooksControllerIndex(): def test_index(self, api, context): serial, books = self.prepare() headers = {'Authorization': context.admin.token} response = hug.test.get(api, '/books', headers=headers) assert response.status == hug.HTTP_OK ... 33
  17. Thank you! (Free hugs!)  @alex_savio  github.com/alexsavio I am

    not responsible if ANY of this code breaks your computer.
  18. Der deutsche Teebeutel (the German tea bag) What's the difference

    between a tea bag and the German football team?
  19. Der deutsche Teebeutel (the German tea bag) What's the difference

    between a tea bag and the German football team? A tea bag stays in the cup for longer...