diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d826197..ca26d3d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,7 @@ jobs: - '3.7' - '3.8' - '3.9' + - '3.10' steps: - name: Checkout code diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index cb876cd..af7c2e0 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,7 +1,8 @@ name: Publish tagged version to PyPI on: - push: + release: + types: [published] jobs: build-publish: diff --git a/README.rst b/README.rst index e990485..62be91b 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -PyTumblr +PyTumblr-Aio ======== |Build Status| @@ -9,7 +9,7 @@ Install via pip: .. code-block:: bash - $ pip install pytumblr + $ pip install PyTumblr-aio Install from source: @@ -36,7 +36,7 @@ A ``pytumblr.TumblrRestClient`` is the object you'll make all of your calls to t '', ) - client.info() # Grabs the current user information + await client.info() # Grabs the current user information Two easy ways to get your credentials to are: @@ -68,14 +68,14 @@ Blog Methods .. code:: python - client.blog_info(blogName) # get information about a blog - client.posts(blogName, **params) # get posts for a blog - client.avatar(blogName) # get the avatar for a blog - client.blog_likes(blogName) # get the likes on a blog - client.followers(blogName) # get the followers of a blog - client.blog_following(blogName) # get the publicly exposed blogs that [blogName] follows - client.queue(blogName) # get the queue for a given blog - client.submission(blogName) # get the submissions for a given blog + await client.blog_info(blogName) # get information about a blog + await client.posts(blogName, **params) # get posts for a blog + await client.avatar(blogName) # get the avatar for a blog + await client.blog_likes(blogName) # get the likes on a blog + await client.followers(blogName) # get the followers of a blog + await client.blog_following(blogName) # get the publicly exposed blogs that [blogName] follows + await client.queue(blogName) # get the queue for a given blog + await client.submission(blogName) # get the submissions for a given blog Post Methods ~~~~~~~~~~~~ @@ -104,18 +104,18 @@ Creating a photo post supports a bunch of different options plus the described d .. code:: python #Creates a photo post using a source URL - client.create_photo(blogName, state="published", tags=["testing", "ok"], - source="https://68.media.tumblr.com/b965fbb2e501610a29d80ffb6fb3e1ad/tumblr_n55vdeTse11rn1906o1_500.jpg") + await client.create_photo(blogName, state="published", tags=["testing", "ok"], + source="https://68.media.tumblr.com/b965fbb2e501610a29d80ffb6fb3e1ad/tumblr_n55vdeTse11rn1906o1_500.jpg") #Creates a photo post using a local filepath - client.create_photo(blogName, state="queue", tags=["testing", "ok"], - tweet="Woah this is an incredible sweet post [URL]", - data="/Users/johnb/path/to/my/image.jpg") + await client.create_photo(blogName, state="queue", tags=["testing", "ok"], + tweet="Woah this is an incredible sweet post [URL]", + data="/Users/johnb/path/to/my/image.jpg") #Creates a photoset post using several local filepaths - client.create_photo(blogName, state="draft", tags=["jb is cool"], format="markdown", - data=["/Users/johnb/path/to/my/image.jpg", "/Users/johnb/Pictures/kittens.jpg"], - caption="## Mega sweet kittens") + await client.create_photo(blogName, state="draft", tags=["jb is cool"], format="markdown", + data=["/Users/johnb/path/to/my/image.jpg", "/Users/johnb/Pictures/kittens.jpg"], + caption="## Mega sweet kittens") Creating a text post '''''''''''''''''''' @@ -125,7 +125,7 @@ Creating a text post supports the same options as default and just a two other p .. code:: python #Creating a text post - client.create_text(blogName, state="published", slug="testing-text-posts", title="Testing", body="testing1 2 3 4") + await client.create_text(blogName, state="published", slug="testing-text-posts", title="Testing", body="testing1 2 3 4") Creating a quote post ''''''''''''''''''''' @@ -135,7 +135,7 @@ Creating a quote post supports the same options as default and two other paramet .. code:: python #Creating a quote post - client.create_quote(blogName, state="queue", quote="I am the Walrus", source="Ringo") + await client.create_quote(blogName, state="queue", quote="I am the Walrus", source="Ringo") Creating a link post '''''''''''''''''''' @@ -147,8 +147,8 @@ Creating a link post .. code:: python #Create a link post - client.create_link(blogName, title="I like to search things, you should too.", url="https://duckduckgo.com", - description="Search is pretty cool when a duck does it.") + await client.create_link(blogName, title="I like to search things, you should too.", url="https://duckduckgo.com", + description="Search is pretty cool when a duck does it.") Creating a chat post '''''''''''''''''''' @@ -172,10 +172,10 @@ Creating an audio post allows for all default options and a has 3 other paramete .. code:: python #Creating an audio file - client.create_audio(blogName, caption="Rock out.", data="/Users/johnb/Music/my/new/sweet/album.mp3") + await client.create_audio(blogName, caption="Rock out.", data="/Users/johnb/Music/my/new/sweet/album.mp3") #lets use soundcloud! - client.create_audio(blogName, caption="Mega rock out.", external_url="https://soundcloud.com/skrillex/sets/recess") + await client.create_audio(blogName, caption="Mega rock out.", external_url="https://soundcloud.com/skrillex/sets/recess") Creating a video post ''''''''''''''''''''' @@ -185,11 +185,11 @@ Creating a video post allows for all default options and has three other options .. code:: python #Creating an upload from YouTube - client.create_video(blogName, caption="Jon Snow. Mega ridiculous sword.", - embed="http://www.youtube.com/watch?v=40pUYLacrj4") + await client.create_video(blogName, caption="Jon Snow. Mega ridiculous sword.", + embed="http://www.youtube.com/watch?v=40pUYLacrj4") #Creating a video post from local file - client.create_video(blogName, caption="testing", data="/Users/johnb/testing/ok/blah.mov") + await client.create_video(blogName, caption="testing", data="/Users/johnb/testing/ok/blah.mov") Editing a post ^^^^^^^^^^^^^^ @@ -198,8 +198,8 @@ Updating a post requires you knowing what type a post you're updating. You'll be .. code:: python - client.edit_post(blogName, id=post_id, type="text", title="Updated") - client.edit_post(blogName, id=post_id, type="photo", data="/Users/johnb/mega/awesome.jpg") + await client.edit_post(blogName, id=post_id, type="text", title="Updated") + await client.edit_post(blogName, id=post_id, type="photo", data="/Users/johnb/mega/awesome.jpg") Reblogging a Post ^^^^^^^^^^^^^^^^^ @@ -208,7 +208,7 @@ Reblogging a post just requires knowing the post id and the reblog key, which is .. code:: python - client.reblog(blogName, id=125356, reblog_key="reblog_key") + await client.reblog(blogName, id=125356, reblog_key="reblog_key") Deleting a post ^^^^^^^^^^^^^^^ @@ -217,13 +217,13 @@ Deleting just requires that you own the post and have the post id .. code:: python - client.delete_post(blogName, 123456) # Deletes your post :( + await client.delete_post(blogName, 123456) # Deletes your post :( A note on tags: When passing tags, as params, please pass them as a list (not a comma-separated string): .. code:: python - client.create_text(blogName, tags=['hello', 'world'], ...) + await client.create_text(blogName, tags=['hello', 'world'], ...) Getting notes for a post ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -232,13 +232,13 @@ In order to get the notes for a post, you need to have the post id and the blog .. code:: python - data = client.notes(blogName, id='123456') + data = await client.notes(blogName, id='123456') The results include a timestamp you can use to make future calls. .. code:: python - data = client.notes(blogName, id='123456', before_timestamp=data["_links"]["next"]["query_params"]["before_timestamp"]) + data = await client.notes(blogName, id='123456', before_timestamp=data["_links"]["next"]["query_params"]["before_timestamp"]) Tagged Methods @@ -247,7 +247,7 @@ Tagged Methods .. code:: python # get posts with a given tag - client.tagged(tag, **params) + await client.tagged(tag, **params) Using the interactive console ----------------------------- diff --git a/pytumblr/__init__.py b/aio_pytumblr/__init__.py similarity index 82% rename from pytumblr/__init__.py rename to aio_pytumblr/__init__.py index 825521c..51c5387 100644 --- a/pytumblr/__init__.py +++ b/aio_pytumblr/__init__.py @@ -1,11 +1,8 @@ -from __future__ import absolute_import -from builtins import str -from builtins import object from .helpers import validate_params, validate_blogname from .request import TumblrRequest -class TumblrRestClient(object): +class TumblrRestClient: """ A Python Client for the Tumblr API """ @@ -30,16 +27,16 @@ def __init__(self, consumer_key, consumer_secret="", oauth_token="", oauth_secre """ self.request = TumblrRequest(consumer_key, consumer_secret, oauth_token, oauth_secret, host) - def info(self): + async def info(self): """ Gets the information about the current given user :returns: A dict created from the JSON response """ - return self.send_api_request("get", "/v2/user/info") + return await self.send_api_request("get", "/v2/user/info") @validate_blogname - def avatar(self, blogname, size=64): + async def avatar(self, blogname, size=64): """ Retrieves the url of the blog's avatar @@ -48,9 +45,9 @@ def avatar(self, blogname, size=64): :returns: A dict created from the JSON response """ url = "/v2/blog/{}/avatar/{}".format(blogname, size) - return self.send_api_request("get", url) + return await self.send_api_request("get", url) - def likes(self, **kwargs): + async def likes(self, **kwargs): """ Gets the current given user's likes :param limit: an int, the number of likes you want returned @@ -63,9 +60,9 @@ def likes(self, **kwargs): :returns: A dict created from the JSON response """ - return self.send_api_request("get", "/v2/user/likes", kwargs, ["limit", "offset", "before", "after"]) + return await self.send_api_request("get", "/v2/user/likes", kwargs, ["limit", "offset", "before", "after"]) - def following(self, **kwargs): + async def following(self, **kwargs): """ Gets the blogs that the current user is following. :param limit: an int, the number of likes you want returned @@ -76,9 +73,9 @@ def following(self, **kwargs): :returns: A dict created from the JSON response """ - return self.send_api_request("get", "/v2/user/following", kwargs, ["limit", "offset"]) + return await self.send_api_request("get", "/v2/user/following", kwargs, ["limit", "offset"]) - def dashboard(self, **kwargs): + async def dashboard(self, **kwargs): """ Gets the dashboard of the current user @@ -91,9 +88,9 @@ def dashboard(self, **kwargs): :returns: A dict created from the JSON response """ - return self.send_api_request("get", "/v2/user/dashboard", kwargs, ["limit", "offset", "type", "since_id", "reblog_info", "notes_info"]) + return await self.send_api_request("get", "/v2/user/dashboard", kwargs, ["limit", "offset", "type", "since_id", "reblog_info", "notes_info"]) - def tagged(self, tag, **kwargs): + async def tagged(self, tag, **kwargs): """ Gets a list of posts tagged with the given tag @@ -108,10 +105,10 @@ def tagged(self, tag, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({'tag': tag}) - return self.send_api_request("get", '/v2/tagged', kwargs, ['before', 'limit', 'filter', 'tag', 'api_key'], True) + return await self.send_api_request("get", '/v2/tagged', kwargs, ['before', 'limit', 'filter', 'tag', 'api_key'], True) @validate_blogname - def posts(self, blogname, type=None, **kwargs): + async def posts(self, blogname, type=None, **kwargs): """ Gets a list of posts from a particular blog @@ -131,10 +128,10 @@ def posts(self, blogname, type=None, **kwargs): url = '/v2/blog/{}/posts'.format(blogname) else: url = '/v2/blog/{}/posts/{}'.format(blogname, type) - return self.send_api_request("get", url, kwargs, ['id', 'tag', 'limit', 'offset', 'before', 'reblog_info', 'notes_info', 'filter', 'api_key', 'npf'], True) + return await self.send_api_request("get", url, kwargs, ['id', 'tag', 'limit', 'offset', 'before', 'reblog_info', 'notes_info', 'filter', 'api_key', 'npf'], True) @validate_blogname - def blog_info(self, blogname): + async def blog_info(self, blogname): """ Gets the information of the given blog @@ -144,10 +141,10 @@ def blog_info(self, blogname): :returns: a dict created from the JSON response of information """ url = "/v2/blog/{}/info".format(blogname) - return self.send_api_request("get", url, {}, ['api_key'], True) + return await self.send_api_request("get", url, {}, ['api_key'], True) @validate_blogname - def blog_following(self, blogname, **kwargs): + async def blog_following(self, blogname, **kwargs): """ Gets the publicly exposed list of blogs that a blog follows @@ -163,10 +160,10 @@ def blog_following(self, blogname, **kwargs): :returns: a dict created from the JSON response """ url = "/v2/blog/{}/following".format(blogname) - return self.send_api_request("get", url, kwargs, ['limit', 'offset']) + return await self.send_api_request("get", url, kwargs, ['limit', 'offset']) @validate_blogname - def followers(self, blogname, **kwargs): + async def followers(self, blogname, **kwargs): """ Gets the followers of the given blog :param limit: an int, the number of followers you want returned @@ -178,10 +175,10 @@ def followers(self, blogname, **kwargs): :returns: A dict created from the JSON response """ url = "/v2/blog/{}/followers".format(blogname) - return self.send_api_request("get", url, kwargs, ['limit', 'offset']) + return await self.send_api_request("get", url, kwargs, ['limit', 'offset']) @validate_blogname - def blog_likes(self, blogname, **kwargs): + async def blog_likes(self, blogname, **kwargs): """ Gets the current given user's likes :param limit: an int, the number of likes you want returned @@ -195,10 +192,10 @@ def blog_likes(self, blogname, **kwargs): :returns: A dict created from the JSON response """ url = "/v2/blog/{}/likes".format(blogname) - return self.send_api_request("get", url, kwargs, ['limit', 'offset', 'before', 'after'], True) + return await self.send_api_request("get", url, kwargs, ['limit', 'offset', 'before', 'after'], True) @validate_blogname - def queue(self, blogname, **kwargs): + async def queue(self, blogname, **kwargs): """ Gets posts that are currently in the blog's queue @@ -209,10 +206,10 @@ def queue(self, blogname, **kwargs): :returns: a dict created from the JSON response """ url = "/v2/blog/{}/posts/queue".format(blogname) - return self.send_api_request("get", url, kwargs, ['limit', 'offset', 'filter', 'npf']) + return await self.send_api_request("get", url, kwargs, ['limit', 'offset', 'filter', 'npf']) @validate_blogname - def drafts(self, blogname, **kwargs): + async def drafts(self, blogname, **kwargs): """ Gets posts that are currently in the blog's drafts :param filter: the post format that you want returned: HTML, text, raw. @@ -220,10 +217,10 @@ def drafts(self, blogname, **kwargs): :returns: a dict created from the JSON response """ url = "/v2/blog/{}/posts/draft".format(blogname) - return self.send_api_request("get", url, kwargs, ['filter', 'npf']) + return await self.send_api_request("get", url, kwargs, ['filter', 'npf']) @validate_blogname - def submission(self, blogname, **kwargs): + async def submission(self, blogname, **kwargs): """ Gets posts that are currently in the blog's submission list @@ -233,10 +230,10 @@ def submission(self, blogname, **kwargs): :returns: a dict created from the JSON response """ url = "/v2/blog/{}/posts/submission".format(blogname) - return self.send_api_request("get", url, kwargs, ['offset', 'filter', 'npf']) + return await self.send_api_request("get", url, kwargs, ['offset', 'filter', 'npf']) @validate_blogname - def follow(self, blogname): + async def follow(self, blogname): """ Follow the url of the given blog @@ -245,10 +242,10 @@ def follow(self, blogname): :returns: a dict created from the JSON response """ url = "/v2/user/follow" - return self.send_api_request("post", url, {'url': blogname}, ['url']) + return await self.send_api_request("post", url, {'url': blogname}, ['url']) @validate_blogname - def unfollow(self, blogname): + async def unfollow(self, blogname): """ Unfollow the url of the given blog @@ -257,9 +254,9 @@ def unfollow(self, blogname): :returns: a dict created from the JSON response """ url = "/v2/user/unfollow" - return self.send_api_request("post", url, {'url': blogname}, ['url']) + return await self.send_api_request("post", url, {'url': blogname}, ['url']) - def like(self, id, reblog_key): + async def like(self, id, reblog_key): """ Like the post of the given blog @@ -270,9 +267,9 @@ def like(self, id, reblog_key): """ url = "/v2/user/like" params = {'id': id, 'reblog_key': reblog_key} - return self.send_api_request("post", url, params, ['id', 'reblog_key']) + return await self.send_api_request("post", url, params, ['id', 'reblog_key']) - def unlike(self, id, reblog_key): + async def unlike(self, id, reblog_key): """ Unlike the post of the given blog @@ -283,10 +280,10 @@ def unlike(self, id, reblog_key): """ url = "/v2/user/unlike" params = {'id': id, 'reblog_key': reblog_key} - return self.send_api_request("post", url, params, ['id', 'reblog_key']) + return await self.send_api_request("post", url, params, ['id', 'reblog_key']) @validate_blogname - def create_photo(self, blogname, **kwargs): + async def create_photo(self, blogname, **kwargs): """ Create a photo post or photoset on a blog @@ -305,10 +302,10 @@ def create_photo(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "photo"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def create_text(self, blogname, **kwargs): + async def create_text(self, blogname, **kwargs): """ Create a text post on a blog @@ -325,10 +322,10 @@ def create_text(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "text"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def create_quote(self, blogname, **kwargs): + async def create_quote(self, blogname, **kwargs): """ Create a quote post on a blog @@ -345,10 +342,10 @@ def create_quote(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "quote"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def create_link(self, blogname, **kwargs): + async def create_link(self, blogname, **kwargs): """ Create a link post on a blog @@ -366,10 +363,10 @@ def create_link(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "link"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def create_chat(self, blogname, **kwargs): + async def create_chat(self, blogname, **kwargs): """ Create a chat post on a blog @@ -386,10 +383,10 @@ def create_chat(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "chat"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def create_audio(self, blogname, **kwargs): + async def create_audio(self, blogname, **kwargs): """ Create a audio post on a blog @@ -407,10 +404,10 @@ def create_audio(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "audio"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def create_video(self, blogname, **kwargs): + async def create_video(self, blogname, **kwargs): """ Create a audio post on a blog @@ -428,10 +425,10 @@ def create_video(self, blogname, **kwargs): :returns: a dict created from the JSON response """ kwargs.update({"type": "video"}) - return self._send_post(blogname, kwargs) + return await self._send_post(blogname, kwargs) @validate_blogname - def reblog(self, blogname, **kwargs): + async def reblog(self, blogname, **kwargs): """ Creates a reblog on the given blogname @@ -448,10 +445,10 @@ def reblog(self, blogname, **kwargs): if 'tags' in kwargs and kwargs['tags']: # Take a list of tags and make them acceptable for upload kwargs['tags'] = ",".join(kwargs['tags']) - return self.send_api_request('post', url, kwargs, valid_options) + return await self.send_api_request('post', url, kwargs, valid_options) @validate_blogname - def delete_post(self, blogname, id): + async def delete_post(self, blogname, id): """ Deletes a post with the given id @@ -461,10 +458,10 @@ def delete_post(self, blogname, id): :returns: a dict created from the JSON response """ url = "/v2/blog/{}/post/delete".format(blogname) - return self.send_api_request('post', url, {'id': id}, ['id']) + return await self.send_api_request('post', url, {'id': id}, ['id']) @validate_blogname - def edit_post(self, blogname, **kwargs): + async def edit_post(self, blogname, **kwargs): """ Edits a post with a given id @@ -486,10 +483,10 @@ def edit_post(self, blogname, **kwargs): kwargs['tags'] = ",".join(kwargs['tags']) valid_options = ['id'] + self._post_valid_options(kwargs.get('type', None)) - return self.send_api_request('post', url, kwargs, valid_options) + return await self.send_api_request('post', url, kwargs, valid_options) @validate_blogname - def notes(self, blogname, id, **kwargs): + async def notes(self, blogname, id, **kwargs): """ Gets the notes @@ -503,7 +500,7 @@ def notes(self, blogname, id, **kwargs): url = "/v2/blog/{}/notes".format(blogname) valid_options = ["id", "mode", "before_timestamp"] kwargs.update({"id":id}) - return self.send_api_request('get', url, kwargs, valid_options) + return await self.send_api_request('get', url, kwargs, valid_options) # Parameters valid for /post, /post/edit, and /post/reblog. def _post_valid_options(self, post_type=None): @@ -528,7 +525,7 @@ def _post_valid_options(self, post_type=None): return valid - def _send_post(self, blogname, params): + async def _send_post(self, blogname, params): """ Formats parameters and sends the API request off. Validates common and per-post-type parameters and formats your tags for you. @@ -546,9 +543,9 @@ def _send_post(self, blogname, params): # Take a list of tags and make them acceptable for upload params['tags'] = ",".join(params['tags']) - return self.send_api_request("post", url, params, valid_options) + return await self.send_api_request("post", url, params, valid_options) - def send_api_request(self, method, url, params={}, valid_parameters=[], needs_api_key=False): + async def send_api_request(self, method, url, params={}, valid_parameters=[], needs_api_key=False): """ Sends the url with parameters to the requested url, validating them to make sure that they are what we expect to have passed to us @@ -575,8 +572,8 @@ def send_api_request(self, method, url, params={}, valid_parameters=[], needs_ap validate_params(valid_parameters, params) if method == "get": - return self.request.get(url, params) + return await self.request.get(url, params) elif method == "delete": - return self.request.delete(url, params) + return await self.request.delete(url, params) else: - return self.request.post(url, params, files) + return await self.request.post(url, params, files) diff --git a/pytumblr/helpers.py b/aio_pytumblr/helpers.py similarity index 100% rename from pytumblr/helpers.py rename to aio_pytumblr/helpers.py diff --git a/pytumblr/request.py b/aio_pytumblr/request.py similarity index 55% rename from pytumblr/request.py rename to aio_pytumblr/request.py index f19467d..d9155f2 100644 --- a/pytumblr/request.py +++ b/aio_pytumblr/request.py @@ -1,17 +1,11 @@ -from future import standard_library -standard_library.install_aliases() -from builtins import object import urllib.parse -import requests +import httpx import sys -PY3 = sys.version_info[0] == 3 +from authlib.integrations.httpx_client import AsyncOAuth1Client -from requests_oauthlib import OAuth1 -from requests.exceptions import TooManyRedirects, HTTPError - -class TumblrRequest(object): +class TumblrRequest: """ A simple request object that lets us query the Tumblr API """ @@ -20,19 +14,18 @@ class TumblrRequest(object): def __init__(self, consumer_key, consumer_secret="", oauth_token="", oauth_secret="", host="https://api.tumblr.com"): self.host = host - self.oauth = OAuth1( - consumer_key, - client_secret=consumer_secret, - resource_owner_key=oauth_token, - resource_owner_secret=oauth_secret - ) + self._oauth_data = {"client_id": consumer_key, "client_secret": consumer_secret, + "token": oauth_token, "token_secret": oauth_secret} self.consumer_key = consumer_key self.headers = { - "User-Agent": "pytumblr/" + self.__version, + "User-Agent": "pytumblr-aio/" + self.__version, } - def get(self, url, params): + def _get_client(self) -> AsyncOAuth1Client: + return AsyncOAuth1Client(**self._oauth_data, headers=self.headers) + + async def get(self, url, params): """ Issues a GET request against the API, properly formatting the params @@ -45,14 +38,11 @@ def get(self, url, params): if params: url = url + "?" + urllib.parse.urlencode(params) - try: - resp = requests.get(url, allow_redirects=False, headers=self.headers, auth=self.oauth) - except TooManyRedirects as e: - resp = e.response + async with self._get_client() as client: + resp = await client.get(url) + return await self.json_parse(resp) - return self.json_parse(resp) - - def post(self, url, params={}, files=[]): + async def post(self, url, params={}, files=[]): """ Issues a POST request against the API, allows for multipart data uploads @@ -64,19 +54,15 @@ def post(self, url, params={}, files=[]): :returns: a dict parsed of the JSON response """ url = self.host + url - try: - if files: - return self.post_multipart(url, params, files) - else: - data = urllib.parse.urlencode(params) - if not PY3: - data = str(data) - resp = requests.post(url, data=data, headers=self.headers, auth=self.oauth) - return self.json_parse(resp) - except HTTPError as e: - return self.json_parse(e.response) - - def delete(self, url, params): + if files: + return await self.post_multipart(url, params, files) + else: + data = urllib.parse.urlencode(params) + async with self._get_client() as client: + resp = await client.post(url, data=data) + return await self.json_parse(resp) + + async def delete(self, url, params): """ Issues a DELETE request against the API, properly formatting the params @@ -89,14 +75,12 @@ def delete(self, url, params): if params: url = url + "?" + urllib.parse.urlencode(params) - try: - resp = requests.delete(url, allow_redirects=False, headers=self.headers, auth=self.oauth) - except TooManyRedirects as e: - resp = e.response - - return self.json_parse(resp) + async with self._get_client() as client: + resp = await client.delete(url) + return await self.json_parse(resp) - def json_parse(self, response): + @staticmethod + async def json_parse(response): """ Wraps and abstracts response validation and JSON parsing to make sure the user gets the correct response. @@ -108,7 +92,8 @@ def json_parse(self, response): try: data = response.json() except ValueError: - data = {'meta': { 'status': 500, 'msg': 'Server Error'}, 'response': {"error": "Malformed JSON or HTML was returned."}} + data = {'meta': {'status': 500, 'msg': 'Server Error'}, + 'response': {"error": "Malformed JSON or HTML was returned."}} # We only really care about the response if we succeed # and the error if we fail @@ -117,7 +102,7 @@ def json_parse(self, response): else: return data - def post_multipart(self, url, params, files): + async def post_multipart(self, url, params, files): """ Generates and issues a multipart request for data files @@ -127,13 +112,11 @@ def post_multipart(self, url, params, files): :returns: a dict parsed from the JSON response """ - resp = requests.post( - url, - data=params, - params=params, - files=files, - headers=self.headers, - allow_redirects=False, - auth=self.oauth - ) - return self.json_parse(resp) + async with self._get_client() as client: + resp = await client.post( + url, + data=params, + params=params, + files=files + ) + return await self.json_parse(resp) diff --git a/interactive_console.py b/interactive_console.py index 1f9db30..8b90190 100644 --- a/interactive_console.py +++ b/interactive_console.py @@ -1,14 +1,9 @@ #!/usr/bin/python -from __future__ import print_function -from future import standard_library -standard_library.install_aliases() -from builtins import input - -import pytumblr +import aio_pytumblr import yaml import os import code -from requests_oauthlib import OAuth1Session +from authlib.integrations.httpx_client import OAuth1Client def new_oauth(yaml_path): @@ -21,18 +16,18 @@ def new_oauth(yaml_path): consumer_key = input('Paste the consumer key here: ').strip() consumer_secret = input('Paste the consumer secret here: ').strip() - request_token_url = 'http://www.tumblr.com/oauth/request_token' - authorize_url = 'http://www.tumblr.com/oauth/authorize' - access_token_url = 'http://www.tumblr.com/oauth/access_token' + request_token_url = 'https://www.tumblr.com/oauth/request_token' + authorize_url = 'https://www.tumblr.com/oauth/authorize' + access_token_url = 'https://www.tumblr.com/oauth/access_token' # STEP 1: Obtain request token - oauth_session = OAuth1Session(consumer_key, client_secret=consumer_secret) + oauth_session = OAuth1Client(consumer_key, client_secret=consumer_secret) fetch_response = oauth_session.fetch_request_token(request_token_url) resource_owner_key = fetch_response.get('oauth_token') resource_owner_secret = fetch_response.get('oauth_token_secret') # STEP 2: Authorize URL + Response - full_authorize_url = oauth_session.authorization_url(authorize_url) + full_authorize_url = oauth_session.create_authorization_url(authorize_url) # Redirect to authentication page print('\nPlease go here and authorize:\n{}'.format(full_authorize_url)) @@ -44,7 +39,7 @@ def new_oauth(yaml_path): verifier = oauth_response.get('oauth_verifier') # STEP 3: Request final access token - oauth_session = OAuth1Session( + oauth_session = OAuth1Client( consumer_key, client_secret=consumer_secret, resource_owner_key=resource_owner_key, @@ -60,12 +55,12 @@ def new_oauth(yaml_path): 'oauth_token_secret': oauth_tokens.get('oauth_token_secret') } - yaml_file = open(yaml_path, 'w+') - yaml.dump(tokens, yaml_file, indent=2) - yaml_file.close() + with open(yaml_path, 'w+') as f: + yaml.dump(tokens, f, indent=2) return tokens + if __name__ == '__main__': yaml_path = os.path.expanduser('~') + '/.tumblr' alternate_path = os.path.expanduser('~') + '/.pytumblr' @@ -76,17 +71,7 @@ def new_oauth(yaml_path): if not os.path.exists(yaml_path): tokens = new_oauth(yaml_path) else: - yaml_file = open(yaml_path, "r") - tokens = yaml.safe_load(yaml_file) - yaml_file.close() - - client = pytumblr.TumblrRestClient( - tokens['consumer_key'], - tokens['consumer_secret'], - tokens['oauth_token'], - tokens['oauth_token_secret'] - ) - - print('pytumblr client created. You may run pytumblr commands prefixed with "client".\n') + with open(yaml_path, "r") as f: + tokens = yaml.safe_load(f) - code.interact(local=dict(globals(), **{'client': client})) + print('pytumblr tokens initialised. You can find tokens here:.\n' + str(yaml_path)) diff --git a/pyproject.toml b/pyproject.toml index 6568bf2..13cba78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,39 +3,38 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "PyTumblr" -version = "0.1.2" +name = "PyTumblr-aio" +version = "0.0.1" authors = [ - { name="Tumblr", email="accounts@tumblr.com" } + { name="Tumblr", email="accounts@tumblr.com" }, + { name="dj-ratty", email="115014503+dj-ratty@users.noreply.github.com"} ] -description = "A Python Tumblr API v2 Client" +description = "An Async Python Tumblr API v2 Client" license = { file="LICENSE" } readme = "README.rst" -requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +requires-python = ">=3.7" classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Natural Language :: English", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7" + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10" ] -keywords = ["pytumblr"] +keywords = ["pytumblr pytumblr-aio aio_pytumblr"] dependencies = [ - "future", - "requests-oauthlib" + "httpx", + "Authlib" ] [project.optional-dependencies] NOSE = ["nose"] NOSECOV = ["nose-cov"] MOCK = ["mock"] +AIOUNITTEST = ["aiounittest"] [project.urls] -"Homepage" = "https://github.com/tumblr/pytumblr" -"Bug Tracker" = "https://github.com/tumblr/pytumblr/issues" -"Download" = "https://github.com/tumblr/pytumblr/releases" +"Homepage" = "https://github.com/dj-ratty/pytumblr-aio" +"Bug Tracker" = "https://github.com/dj-ratty/pytumblr-aio/issues" +"Download" = "https://github.com/dj-ratty/pytumblr-aio/releases" diff --git a/setup.py b/setup.py index b5c9665..176155c 100644 --- a/setup.py +++ b/setup.py @@ -90,44 +90,42 @@ def run(self): setup( - name="PyTumblr", - version="0.1.2", - description="A Python API v2 wrapper for Tumblr", + name="PyTumblr-aio", + version="0.0.1", + description="An Async Python API v2 wrapper for Tumblr", long_description=long_description, - author="Tumblr", - author_email="accounts@tumblr.com", - url="https://github.com/tumblr/pytumblr", + author="dj-ratty", + author_email="115014503+dj-ratty@users.noreply.github.com", + url="https://github.com/dj-ratty/pytumblr-aio", download_url="https://github.com/tumblr/pytumblr/archive/0.1.1.tar.gz", - packages=['pytumblr'], + packages=['aio_pytumblr'], license="Apache Software License 2.0", zip_safe=False, - keywords='pytumblr', - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + keywords='pytumblr pytumblr-aio aio_pytumblr', + python_requires=">=3.7", classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', - "Programming Language :: Python :: 2", - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10' ], test_suite='nose.collector', install_requires=[ - 'future', - 'requests-oauthlib', + 'httpx', + 'Authlib', ], tests_require=[ 'nose', 'nose-cov', - 'mock' + 'mock', + 'aiounittest' ], cmdclass={ diff --git a/tests/test_pytumblr.py b/tests/test_pytumblr.py index aa38556..cf09e00 100644 --- a/tests/test_pytumblr.py +++ b/tests/test_pytumblr.py @@ -1,11 +1,9 @@ -from __future__ import unicode_literals -from future import standard_library -standard_library.install_aliases() import unittest import mock import json -import pytumblr +import aio_pytumblr from urllib.parse import parse_qs +import aiounittest def wrap_response(response_text): @@ -27,305 +25,305 @@ def inner(*args, **kwargs): return inner -class TumblrRestClientTest(unittest.TestCase): +class TumblrRestClientTest(aiounittest.AsyncTestCase): """ """ def setUp(self): - with open('tests/tumblr_credentials.json', 'r') as f: + with open('tumblr_credentials.json', 'r') as f: credentials = json.loads(f.read()) - self.client = pytumblr.TumblrRestClient(credentials['consumer_key'], credentials['consumer_secret'], credentials['oauth_token'], credentials['oauth_token_secret']) + self.client = aio_pytumblr.TumblrRestClient(credentials['consumer_key'], credentials['consumer_secret'], credentials['oauth_token'], credentials['oauth_token_secret']) - @mock.patch('requests.get') - def test_dashboard(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_dashboard(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') - response = self.client.dashboard() + response = await self.client.dashboard() assert response['posts'] == [] - @mock.patch('requests.get') - def test_posts(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_posts(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') - response = self.client.posts('codingjester.tumblr.com') + response = await self.client.posts('codingjester.tumblr.com') assert response['posts'] == [] - @mock.patch('requests.get') - def test_posts_with_type(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_posts_with_type(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') - response = self.client.posts('seejohnrun', 'photo') + response = await self.client.posts('seejohnrun', 'photo') assert response['posts'] == [] - @mock.patch('requests.get') - def test_posts_with_type_and_arg(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_posts_with_type_and_arg(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') args = {'limit': 1} - response = self.client.posts('seejohnrun', 'photo', **args) + response = await self.client.posts('seejohnrun', 'photo', **args) assert response['posts'] == [] - @mock.patch('requests.get') - def test_blogInfo(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_blogInfo(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"blog": {} } }') - response = self.client.blog_info('codingjester.tumblr.com') + response = await self.client.blog_info('codingjester.tumblr.com') assert response['blog'] == {} - @mock.patch('requests.get') - def test_avatar_with_301(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_avatar_with_301(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 301, "msg": "Moved Permanently"}, "response": {"avatar_url": "" } }') - response = self.client.avatar('staff.tumblr.com') + response = await self.client.avatar('staff.tumblr.com') assert response['avatar_url'] == '' - @mock.patch('requests.get') - def test_avatar_with_302(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_avatar_with_302(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 302, "msg": "Found"}, "response": {"avatar_url": "" } }') - response = self.client.avatar('staff.tumblr.com') + response = await self.client.avatar('staff.tumblr.com') assert response['avatar_url'] == '' - @mock.patch('requests.get') - def test_followers(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_followers(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"users": [] } }') - response = self.client.followers('codingjester.tumblr.com') + response = await self.client.followers('codingjester.tumblr.com') assert response['users'] == [] - @mock.patch('requests.get') - def test_blog_following(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_blog_following(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"blogs": [], "total_blogs": 1}}') - response = self.client.blog_following('pytblr.tumblr.com') + response = await self.client.blog_following('pytblr.tumblr.com') assert response['blogs'] == [] - @mock.patch('requests.get') - def test_blogLikes(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_blogLikes(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"liked_posts": [] } }') - response = self.client.blog_likes('codingjester.tumblr.com') + response = await self.client.blog_likes('codingjester.tumblr.com') assert response['liked_posts'] == [] - @mock.patch('requests.get') - def test_blogLikes_with_after(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_blogLikes_with_after(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"liked_posts": [] } }') - response = self.client.blog_likes('codingjester.tumblr.com', after=1418684291) + response = await self.client.blog_likes('codingjester.tumblr.com', after=1418684291) assert response['liked_posts'] == [] - @mock.patch('requests.get') - def test_notes(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_notes(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"notes": [], "total_notes": 1, "can_hide_or_delete_notes": false} }') - response = self.client.notes('codingjester.tumblr.com', id='123456789098') + response = await self.client.notes('codingjester.tumblr.com', id='123456789098') assert response["notes"] == [] - @mock.patch('requests.get') - def test_blogLikes_with_before(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_blogLikes_with_before(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"liked_posts": [] } }') - response = self.client.blog_likes('codingjester.tumblr.com', before=1418684291) + response = await self.client.blog_likes('codingjester.tumblr.com', before=1418684291) assert response['liked_posts'] == [] - @mock.patch('requests.get') - def test_queue(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_queue(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') - response = self.client.queue('codingjester.tumblr.com') + response = await self.client.queue('codingjester.tumblr.com') assert response['posts'] == [] - @mock.patch('requests.get') - def test_drafts(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_drafts(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') - response = self.client.drafts('codingjester.tumblr.com') + response = await self.client.drafts('codingjester.tumblr.com') assert response['posts'] == [] - @mock.patch('requests.get') - def test_submissions(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_submissions(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": {"posts": [] } }') - response = self.client.submission('codingjester.tumblr.com') + response = await self.client.submission('codingjester.tumblr.com') assert response['posts'] == [] - @mock.patch('requests.post') - def test_follow(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_follow(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 200, "msg": "OK"}, "response": []}', mock_post) - response = self.client.follow("codingjester.tumblr.com") + response = await self.client.follow("codingjester.tumblr.com") assert response == [] assert parse_qs(mock_post.data) == parse_qs('url=codingjester.tumblr.com') - @mock.patch('requests.post') - def test_unfollow(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_unfollow(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 200, "msg": "OK"}, "response": []}', mock_post) - response = self.client.unfollow("codingjester.tumblr.com") + response = await self.client.unfollow("codingjester.tumblr.com") assert response == [] assert parse_qs(mock_post.data) == parse_qs('url=codingjester.tumblr.com') - @mock.patch('requests.post') - def test_reblog(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_reblog(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 200, "msg": "OK"}, "response": []}', mock_post) - response = self.client.reblog('seejohnrun', id='123', reblog_key="adsfsadf", state='coolguy', tags=['hello', 'world']) + response = await self.client.reblog('seejohnrun', id='123', reblog_key="adsfsadf", state='coolguy', tags=['hello', 'world']) assert response == [] assert parse_qs(mock_post.data) == parse_qs('state=coolguy&reblog_key=adsfsadf&id=123&tags=hello%2Cworld') - @mock.patch('requests.post') - def test_edit_post(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_edit_post(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 200, "msg": "OK"}, "response": []}', mock_post) - response = self.client.edit_post('seejohnrun', id='123', state='coolguy', tags=['hello', 'world']) + response = await self.client.edit_post('seejohnrun', id='123', state='coolguy', tags=['hello', 'world']) assert response == [] assert parse_qs(mock_post.data) == parse_qs('state=coolguy&id=123&tags=hello%2Cworld') - @mock.patch('requests.post') - def test_like(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_like(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 200, "msg": "OK"}, "response": []}', mock_post) - response = self.client.like('123', "adsfsadf") + response = await self.client.like('123', "adsfsadf") assert response == [] assert parse_qs(mock_post.data) == parse_qs('id=123&reblog_key=adsfsadf') - @mock.patch('requests.post') - def test_unlike(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_unlike(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 200, "msg": "OK"}, "response": []}', mock_post) - response = self.client.unlike('123', "adsfsadf") + response = await self.client.unlike('123', "adsfsadf") assert response == [] assert parse_qs(mock_post.data) == parse_qs('id=123&reblog_key=adsfsadf') - @mock.patch('requests.get') - def test_info(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_info(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') - response = self.client.info() + response = await self.client.info() assert response == [] - @mock.patch('requests.get') - def test_likes(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_likes(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') - response = self.client.likes() + response = await self.client.likes() assert response == [] - @mock.patch('requests.get') - def test_likes_with_after(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_likes_with_after(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') - response = self.client.likes(after=1418684291) + response = await self.client.likes(after=1418684291) assert response == [] - @mock.patch('requests.get') - def test_likes_with_before(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_likes_with_before(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') - response = self.client.likes(before=1418684291) + response = await self.client.likes(before=1418684291) assert response == [] - @mock.patch('requests.get') - def test_following(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_following(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') - response = self.client.following() + response = await self.client.following() assert response == [] - @mock.patch('requests.get') - def test_tagged(self, mock_get): + @mock.patch('httpx.AsyncClient.request') + async def test_tagged(self, mock_get): mock_get.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') - response = self.client.tagged('food') + response = await self.client.tagged('food') assert response == [] - @mock.patch('requests.post') - def test_create_text(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_text(self, mock_post): mock_post.side_effect = wrap_response('{"meta": {"status": 201, "msg": "OK"}, "response": []}') - response = self.client.create_text('codingjester.tumblr.com', body="Testing") + response = await self.client.create_text('codingjester.tumblr.com', body="Testing") assert response == [] - @mock.patch('requests.post') - def test_create_link(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_link(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 201, "msg": "OK"}, "response": []}', mock_post) - response = self.client.create_link('codingjester.tumblr.com', url="https://google.com", tags=['omg', 'nice']) + response = await self.client.create_link('codingjester.tumblr.com', url="https://google.com", tags=['omg', 'nice']) assert response == [] assert parse_qs(mock_post.data) == parse_qs('url=https%3A%2F%2Fgoogle.com&type=link&tags=omg%2Cnice') - @mock.patch('requests.post') - def test_no_tags(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_no_tags(self, mock_post): mock_post.side_effect = wrap_response_storing_data( '{"meta": {"status": 201, "msg": "OK"}, "response": []}', mock_post) - self.client.create_link('seejohnrun.tumblr.com', tags=[]) + await self.client.create_link('seejohnrun.tumblr.com', tags=[]) assert parse_qs(mock_post.data) == parse_qs('type=link&tags=[]') - @mock.patch('requests.post') - def test_create_quote(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_quote(self, mock_post): mock_post.side_effect = wrap_response('{"meta": {"status": 201, "msg": "OK"}, "response": []}') - response = self.client.create_quote('codingjester.tumblr.com', quote="It's better to love and lost, than never have loved at all.") + response = await self.client.create_quote('codingjester.tumblr.com', quote="It's better to love and lost, than never have loved at all.") assert response == [] - @mock.patch('requests.post') - def test_create_chat(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_chat(self, mock_post): mock_post.side_effect = wrap_response('{"meta": {"status": 201, "msg": "OK"}, "response": []}') - response = self.client.create_chat('codingjester.tumblr.com', conversation="JB: Testing is rad.\nJC: Hell yeah.") + response = await self.client.create_chat('codingjester.tumblr.com', conversation="JB: Testing is rad.\nJC: Hell yeah.") assert response == [] - @mock.patch('requests.post') - def test_create_photo(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_photo(self, mock_post): mock_post.side_effect = wrap_response('{"meta": {"status": 201, "msg": "OK"}, "response": []}') - response = self.client.create_photo('codingjester.tumblr.com', source="https://media.tumblr.com/image.jpg") + response = await self.client.create_photo('codingjester.tumblr.com', source="https://media.tumblr.com/image.jpg") assert response == [] - @mock.patch('requests.post') - def test_create_audio(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_audio(self, mock_post): mock_post.side_effect = wrap_response('{"meta": {"status": 201, "msg": "OK"}, "response": []}') - response = self.client.create_audio('codingjester.tumblr.com', external_url="https://media.tumblr.com/audio.mp3") + response = await self.client.create_audio('codingjester.tumblr.com', external_url="https://media.tumblr.com/audio.mp3") assert response == [] - @mock.patch('requests.post') - def test_create_video(self, mock_post): + @mock.patch('httpx.AsyncClient.request') + async def test_create_video(self, mock_post): mock_post.side_effect = wrap_response('{"meta": {"status": 201, "msg": "OK"}, "response": []}') - response = self.client.create_video('codingjester.tumblr.com', embed="blahblahembed") + response = await self.client.create_video('codingjester.tumblr.com', embed="blahblahembed") assert response == [] - @mock.patch('requests.delete') - def test_api_delete(self, mock_delete): + @mock.patch('httpx.AsyncClient.request') + async def test_api_delete(self, mock_delete): mock_delete.side_effect = wrap_response('{"meta": {"status": 200, "msg": "OK"}, "response": []}') api_url = '/v2/some/api' - response = self.client.send_api_request('delete', api_url, {'param1': 'foo', 'param2': 'bar'}, ['param1', 'param2'], False) + response = await self.client.send_api_request('delete', api_url, {'param1': 'foo', 'param2': 'bar'}, ['param1', 'param2'], False) assert response == []