A compact toolkit for wrapping web APIs
Project description
Snug is a compact toolkit for wrapping web APIs.
Architecture agnostic (REST, RPC, GraphQL, …)
Swappable HTTP clients (urllib, requests, aiohttp, …)
Interchangeably sync/async
Quickstart
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)
Queries can be executed:
>>> query = repo('Hello-World', owner='octocat') >>> snug.execute(query) {"description": "My first repository on Github!", ...}
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
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))
Effortlessly async. 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 standard library:
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(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!", ...})
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 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)))
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
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.