Skip to main content

Write reusable web API interactions

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://img.shields.io/travis/ariebovenberg/snug.svg https://img.shields.io/codecov/c/github/ariebovenberg/snug.svg https://img.shields.io/readthedocs/snug.svg https://img.shields.io/codeclimate/maintainability/ariebovenberg/snug.svg

Snug is a tiny toolkit for writing reusable interactions with web APIs. Key features:

  • Write once, run with different HTTP clients (sync and async)

  • Fits any API architecture (e.g. REST, RPC, GraphQL)

  • Simple, lightweight and versatile

Why?

Writing reusable web API interactions is difficult. Consider a generic example:

import json

def repo(name, owner):
    """get a github repo by owner and name"""
    request = Request(f'https://api.github.com/repos/{owner}/{name}')
    response = my_http_client.send(request)
    return json.loads(response.content)

Nice and simple. But…

  • What about async? Do we write another function for that?

  • How do we write clean unittests for this?

  • What if we want to use another HTTP client or session?

  • How do we use this with different credentials?

Snug allows you to write API interactions independent of HTTP client, credentials, or whether they are run (a)synchronously.

In contrast to most API client toolkits, snug makes minimal assumptions and design decisions for you. Its simple, adaptable foundation ensures you can focus on what makes your API unique. Snug fits in nicely whether you’re writing a full-featured API wrapper, or just making a few API calls.

Quickstart

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

import snug

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

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

Features

  1. Effortlessly async. The same query can also be executed asynchronously:

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

    import requests
    query = repo('Hello-World', owner='octocat')
    snug.execute(query, client=requests.Session())
  4. Testability. Queries can easily be run without touching the network. No need for complex mocks or monkeypatching.

    >>> query = 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. Queries aren’t tied to a session or credentials. Use different credentials 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
    
    snug.execute(follow('octocat'), auth=('me', 'password'))
    snug.execute(follow('octocat'), auth=('bob', 'hunter2'))
  6. Related queries. Use class-based queries to create an expressive, 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 for 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)
    
    my_issue = repo('Hello-World', owner='octocat').issue(348)
    snug.execute(my_issue)
  7. Pagination. Define paginated queries for (asynchronous) iteration.

    def organizations(since: int=None):
        """retrieve a page of organizations since a particular id"""
        resp = yield snug.GET('https://api.github.com/organizations',
                              params={'since': since} if since else {})
        orgs = json.loads(resp.content)
        next_query = organizations(since=orgs[-1]['id'])
        return snug.Page(orgs, next_query=next_query)
    
    my_query = snug.paginated(organizations())
    
    for orgs in snug.execute(my_query):
        ...
    
    # or, with async
    async for orgs in snug.execute_async(my_query):
        ...
  8. Function- or class-based? You decide. One option to keep everything DRY is to use class-based queries and inheritance:

    class BaseQuery(snug.Query):
        """base github query"""
    
        def prepare(self, request): ...  # add url prefix, headers, etc.
    
        def __iter__(self):
            """the base query routine"""
            request = self.prepare(self.request)
            return self.load(self.check_response((yield request)))
    
        def check_response(self, result): ...  # raise nice errors
    
    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 higher-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, advanced features, recipes, or examples.

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 aiohttp

Python 2

Writing python2-compatible queries is supported, with two important caveats:

  1. Returning values from generators is not natively supported in python2. Use the py2_compatible decorator (from gentools) to do this. The resulting query can be run on python 2 and 3.

from gentools import py2_compatible, return_

@py2_compatible
def repo(name, owner):
    """get a github repo by owner and name"""
    request = snug.GET(f'https://api.github.com/repos/{owner}/{name}')
    response = yield request
    return_(json.loads(response.content))
  1. Async functionality is not available on python2. Python2-compatible queries will be able to be run asychronously on python3.

Alternatives

If you’re looking for a less minimalistic API client toolkit, check out uplink or tapioca.

Release history

development

1.3.0 (2018-05-13)

  • remove deprecated auth_method parameter in execute()

1.2.1 (2018-03-26)

  • fix in README

1.2.0 (2018-03-21)

  • auth parameter accepts callables

  • deprecate auth_method parameter (to remove in version 1.3)

  • paginated queries

  • make asyncio client more robust

  • added two new recipes

1.1.3 (2018-03-07)

  • remove tutorial directory from build

1.1.2 (2018-03-07)

  • fixes to docs

1.1.1 (2018-03-04)

  • fixes to docs

1.1.0 (2018-03-04)

  • python 2 compatibility

  • implement overridable __execute__, __execute_async__

  • improvements to aiohttp, urllib clients

1.0.2 (2018-02-18)

  • fixes for sending requests with default clients

  • improvements to docs

1.0.1 (2018-02-12)

  • improvements to docs

  • fix for send_async

1.0.0 (2018-02-09)

  • improvements to docs

  • added slack API example

  • related decorator replaces Relation query class

  • bugfixes

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-1.3.0.tar.gz (16.6 kB view details)

Uploaded Source

Built Distribution

snug-1.3.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file snug-1.3.0.tar.gz.

File metadata

  • Download URL: snug-1.3.0.tar.gz
  • Upload date:
  • Size: 16.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for snug-1.3.0.tar.gz
Algorithm Hash digest
SHA256 491fd892c8689716d824b19878f0a06017338aa80a8420a204e58fb386fbf3ef
MD5 4a6c740ee1ffd8a8f7a148572d6ffd60
BLAKE2b-256 34432271d54e1c525aba2843ea0190d26f2b318a64d4090a1153613ac7c11035

See more details on using hashes here.

File details

Details for the file snug-1.3.0-py3-none-any.whl.

File metadata

  • Download URL: snug-1.3.0-py3-none-any.whl
  • Upload date:
  • Size: 20.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for snug-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6ff531d97601439afd6bcc3d5c79d9303ff2022ad8348d494b2f582be078394a
MD5 294ebb26672a1d0c742bff31d17001d5
BLAKE2b-256 24124bc33e67208ac4a54af000059c97dd96541ef7b47e44739255dd6cdd27d1

See more details on using hashes here.

Supported by

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