Skip to main content

A compact toolkit for wrapping web APIs

Project description

https://img.shields.io/pypi/v/snug.svg https://img.shields.io/pypi/l/snug.svg https://img.shields.io/pypi/pyversions/snug.svg https://travis-ci.org/ariebovenberg/snug.svg?branch=master https://coveralls.io/repos/github/ariebovenberg/snug/badge.svg?branch=master Documentation Status Maintainability

Snug is a compact toolkit for wrapping web APIs.

  • Architecture agnostic (REST, RPC, GraphQL, …)

  • Swappable HTTP clients (urllib, requests, aiohttp, …)

  • Interchangeably sync/async

Quickstart

  1. API interactions (“queries”) are request/response generators:

    import json
    import snug
    
    def repo(name, owner):
        """a repo lookup by owner and name"""
        request = snug.GET(f'https://api.github.com/repos/{owner}/{name}')
        response = yield request
        return json.loads(response.content)
  2. Queries can be executed:

    >>> query = repo('Hello-World', owner='octocat')
    >>> snug.execute(query)
    {"description": "My first repository on Github!", ...}
  3. That’s it

Why another library?

There are plenty of tools for wrapping web APIs. However, these generally make far-reaching design decisions for you, making it awkward to bend it to the needs of a specific API. Snug aims only to provide a versatile base, so you can focus on what makes your API unique.

Installation

There are no required dependencies on python 3.5+. Installation is easy as:

pip install snug

Although snug includes basic sync and async HTTP clients, you may wish to install requests and/or aiohttp.

pip install requests
pip install aiohttp

Features

  1. Flexibility. Since queries are just generators, customizing them requires no special glue-code. For example: add validation logic, or use any serialization method:

    from my_types import User, UserSchema
    
    def user(name: str) -> snug.Query[User]:
        """lookup a user by their username"""
        if len(name) == 0:
            raise ValueError('username must have >0 characters')
        request = snug.GET(f'https://api.github.com/users/{name}')
        response = yield request
        return UserSchema().load(json.loads(response.content))
  2. Effortlessly async. The same query can also be executed asynchronously:

    query = repo('Hello-World', owner='octocat')
    repo = await snug.execute_async(query)
  3. Pluggable clients. Queries are fully agnostic of the HTTP client. For example, to use requests instead of the standard library:

    import requests
    execute = snug.executor(client=requests.Session())
    execute(repo('Hello-World', owner='octocat'))
    # {"description": "My first repository on Github!", ...}
  4. Testable. Since queries are just generators, we can run them just fine without touching the network. No need for complex mocks or monkeypatching.

    >>> query = iter(repo('Hello-World', owner='octocat'))
    >>> next(query).url.endswith('/repos/octocat/Hello-World')
    True
    >>> query.send(snug.Response(200, b'...'))
    StopIteration({"description": "My first repository on Github!", ...})
  5. Swappable authentication. Different credentials can be used to execute the same query:

    def follow(name: str) -> snug.Query[bool]:
        """follow another user"""
        req = snug.PUT('https://api.github.com/user/following/{name}')
        return (yield req).status_code == 204
    
    exec_as_me = snug.executor(auth=('me', 'password'))
    exec_as_bob = snug.executor(auth=('bob', 'password'))
    
    exec_as_me(follow('octocat'))
    exec_as_bob(follow('octocat'))
  6. Related queries. Use class-based queries to create a chained API for related objects:

    class repo(snug.Query[dict]):
        """a repo lookup by owner and name"""
        def __init__(self, name, owner): ...
    
        def __iter__(self): ...  # query of the repo itself
    
        def issue(self, num: int) -> snug.Query[dict]:
            """retrieve an issue in this repository by its number"""
            r = snug.GET(f'/repos/{self.owner}/{self.name}/issues/{num}')
            return json.loads((yield r).content)
    
    hello_world_repo = repo('Hello-World', owner='octocat')
    issue_348 = hello_world_repo.issue(348)
    snug.execute(issue_348)
    # {"title": "Testing comments", ...}
    
    # we could take this as far as we like, eventually:
    new_comments = (repo('Hello-World', owner='octocat')
                    .issue(348)
                    .comments(since=datetime(2018, 1, 1)))
  7. Function- or class-based? You decide. Use class-based queries and inheritance to keep everything DRY:

    class BaseQuery(snug.Query):
        """base github query"""
    
        def prepare(self, request): ...  # add url prefix, headers, etc.
    
        def __iter__(self):
            request = self.prepare(self.request)
            return self.load(self.check_response((yield request)))
    
        def check_response(self, result): ...
    
    class repo(BaseQuery):
        """get a repo by owner and name"""
        def __init__(self, name, owner):
            self.request = snug.GET(f'/repos/{owner}/{name}')
    
        def load(self, response):
            return my_repo_loader(response.content)
    
    class follow(BaseQuery):
        """follow another user"""
        def __init__(self, name):
            self.request = snug.PUT(f'/user/following/{name}')
    
        def load(self, response):
            return response.status_code == 204

    Or, if you’re comfortable with high-order functions and decorators, make use of gentools to modify query yield, send, and return values:

    from gentools import (map_return, map_yield, map_send,
                          compose, oneyield)
    
    class Repository: ...
    
    def my_repo_loader(...): ...
    
    def my_error_checker(...): ...
    
    def my_request_preparer(...): ...  # add url prefix, headers, etc.
    
    basic_interaction = compose(map_send(my_error_checker),
                                map_yield(my_request_preparer))
    
    @map_return(my_repo_loader)
    @basic_interaction
    @oneyield
    def repo(owner: str, name: str) -> snug.Query[Repository]:
        """get a repo by owner and name"""
        return snug.GET(f'/repos/{owner}/{name}')
    
    @basic_interaction
    def follow(name: str) -> snug.Query[bool]:
        """follow another user"""
        response = yield snug.PUT(f'/user/following/{name}')
        return response.status_code == 204

For more info, check out the tutorial, recipes, or the examples (in the examples/ directory)

Release history

development

0.5.0 (2018-01-30)

  • improvements to docs

  • rename Request/Response data->content

  • Relation query class

0.4.0 (2018-01-24)

  • removed generator utils and serialization logic (now seperate libraries)

  • improvements to docs

0.3.0 (2018-01-14)

  • generator-based queries

0.1.2

  • fixes to documentation

0.1.1

  • improvements to versioning info

0.1.0

  • implement basic resource and simple example

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snug-0.5.0.tar.gz (15.6 kB view hashes)

Uploaded Source

Built Distribution

snug-0.5.0-py3-none-any.whl (18.7 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page