A compact toolkit for wrapping web APIs
Project description
Snug is a compact toolkit for wrapping web APIs. Architecture agnostic, pluggable, and interchangeably sync/async. Write API interactions as regular python code.
Quickstart
API interactions (“queries”) are request/response generators:
import json import snug def repo(name: str, owner: str) -> snug.Query[dict]: """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.data)
Queries can be executed:
>>> query = repo('Hello-World', owner='octocat') >>> snug.execute(query) {"description": "My first repository on Github!", ...}
That’s it
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
Simplicity. If you understand generators, you understand queries. Customizing a query 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.data))
Async out-of-the-box. The same query can also be executed asynchronously:
query = repo('Hello-World', owner='octocat') repo = await snug.execute_async(query)
Pluggable clients. Queries are fully agnostic of the HTTP client. For example, to use requests instead of the built-in urllib:
>>> import requests >>> execute = snug.executor(client=requests.Session()) >>> execute(repo('Hello-World', owner='octocat')) {"description": "My first repository on Github!", ...}
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(epo('Hello-World', owner='octocat')) >>> next(query).url.endswith('/repos/octocat/Hello-World') True >>> query.send(snug.Response(200, ...)) StopIteration({"description": "My first repository on Github!", ...})
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'))
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 for the repo itself def issue(self, num: int) -> snug.Query[dict]: """retrieve an issue in this repository by its number""" req = snug.GET(f'/repos/{self.owner}/{self.name}/issues/{num}') return json.loads((yield req).data) # the `repo` query works as before hello_world_repo = repo('Hello-World', owner='octocat') # ...but now we can make a related query issue_lookup = hello_world_repo.issue(348) snug.execute(issue_lookup) # {"title": "Testing comments", ...}
Composable. If you’re comfortable with high-order functions and decorators, make use of gentools to create generators and apply functions to a generator’s yield, send, and return values.
from gentools import (map_return, map_yield, map_send, compose, oneyield) class Repository: ... def my_repo_loader(...): ... # e.g. create a nice `Repository` object def my_error_checker(...): ... # e.g. raise descritive errors on HTTP 4xx responses def my_request_preparer(...): ... # e.g. add headers, url prefix, 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
Alternatively, use a class-based approach:
class BaseQuery(snug.Query): """base github query""" def prepare(self, request): ... # e.g. add headers, url prefix, etc def __iter__(self): return parse_result((yield self.prepare(self.request))) def parse_result(self, result): ... # e.g. error checking class repo(BaseQuery): """get a repo by owner and name""" def __init__(self, name, owner): self.request = snug.GET(f'/repos/{owner}/{name}') def parse_result(self, result): result = super().parse_result(result) return my_repo_loader(result.data) class follow(BaseQuery): """follow another user""" def __init__(self, name): self.request = snug.PUT(f'/user/following/{name}') def parse_result(self, result): result = super().parse_result(result) return result.status_code == 204
Check the examples/ directory for some samples.
Release history
development
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.