Snug đź§Ł#
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#
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)
Queries can be executed:
>>> query = repo('Hello-World', owner='octocat')
>>> snug.execute(query)
{"description": "My first repository on Github!", ...}
Features#
Effortlessly async. The same query can also be executed asynchronously:
query = repo('Hello-World', owner='octocat') repo = await snug.execute_async(query)
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))
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())
Read here how to register your own.
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!", ...})
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'))
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)
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): ...
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
, andreturn
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. Installation is easy as:
pip install snug
Although snug includes basic sync and async HTTP clients, you may wish to install requests, httpx, and/or aiohttp.
pip install requests aiohttp httpx
Alternatives#
If you’re looking for a less minimalistic API client toolkit, check out uplink or tapioca.
Contents#
Release history#
development#
2.4.0 (2022-10-28)#
Official Python 3.11 support, remove Python 3.6 support.
Update docs theme
2.3.1 (2022-01-20)#
Add missing
__slots__
toQuery
class
2.3.0 (2021-11-28)#
Added support for
httpx
clientFixed
asyncio
deprecation warningAdded official Python 3.10 support
2.2.0 (2021-05-14)#
Update build with poetry, github actions
2.1.0 (2020-12-04)#
Add Python 3.9 support, drop 3.5.
2.0.0 (2019-10-27)#
Add Python 3.8 support
Drop Python 2 support
Adopt
black
autoformatterFix error on import when no event loop is available
1.4.1 (2019-03-30)#
Small fix in pypi package metadata
1.4.0 (2019-03-30)#
Drop python 3.4 support (#83)
1.3.4 (2018-10-27)#
Fix deprecation warning on python 3.7 (#35)
1.3.3 (2018-10-25)#
Fix issue where
urllib
client would raiseHTTPError
on HTTP error status codes (#33).
1.3.2 (2018-08-27)#
improvements to documentation
1.3.1 (2018-08-25)#
official python 3.7 support
small fixes to documentation
1.3.0 (2018-05-13)#
remove deprecated
auth_method
parameter inexecute()
1.2.1 (2018-03-26)#
fix in README
1.2.0 (2018-03-21)#
auth
parameter accepts callablesdeprecate
auth_method
parameter (to remove in version 1.3)paginated queries
make
asyncio
client more robustadded 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 replacesRelation
query classbugfixes
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