diff --git a/README.md b/README.md index 6b60b0f..5823f75 100644 --- a/README.md +++ b/README.md @@ -7,38 +7,48 @@ For full API documentation, see our [developer docs](https://github.com/Medium/m ## Usage -```python -from medium import Client - -# Contact developers@medium.com to get your application_kd and application_secret. -client = Client(application_id="MY_APPLICATION_ID", application_secret="MY_APPLICATION_SECRET") - -# Build the URL where you can send the user to obtain an authorization code. -auth_url = client.get_authorization_url("secretstate", "https://yoursite.com/callback/medium", - ["basicProfile", "publishPost"]) - -# (Send the user to the authorization URL to obtain an authorization code.) -# Exchange the authorization code for an access token. -auth = client.exchange_authorization_code("YOUR_AUTHORIZATION_CODE", - "https://yoursite.com/callback/medium") -# The access token is automatically set on the client for you after -# a successful exchange, but if you already have a token, you can set it -# directly. -client.access_token = auth["access_token"] +To use the client, you first need to obtain an access token, which requires +an authorization code from the user. Send them to the URL given by +``Client.get_authorization_url()`` and then have them enter their +authorization code to exchange it for an access token. -# Get profile details of the user identified by the access token. -user = client.get_current_user() - -# Create a draft post. -post = client.create_post(user_id=user["id"], title="Title", content="
Content
", - content_format="html", publish_status="draft") - -# When your access token expires, use the refresh token to get a new one. -client.exchange_refresh_token(auth["refresh_token"]) +```python +from medium import Client -# Confirm everything went ok. post["url"] has the location of the created post. +# Contact developers@medium.com to get your application_kd and application_secret. +client = Client(application_id="MY_APPLICATION_ID", + application_secret="MY_APPLICATION_SECRET") + +# Obtain an access token, by sending the user to the authorization URL and +# exchanging their authorization code for an access token. + +redirect_url = "https://yoursite.com/callback/medium" +authorize_url = client.get_authorization_url( + state="secretstate", + redirect_url=redirect_url, + scopes=["basicProfile", "publishPost"] +) +print 'Go to: {}'.format(authorize_url) +print 'Copy the authorization code.' +authorization_code = raw_input('Enter the authorization code here: ') +client.exchange_authorization_code(authorization_code, redirect_url) + +# Now that you have an access token, you can use the rest of the client's +# methods. For example, to get the profile details of user identified by the +# access token: + +print client.get_current_user() + +# And to create a draft post: + +post = client.create_post( + user_id=user["id"], + title="Title", + content="Content
", + content_format="html", publish_status="draft" +) print "My new post!", post["url"] ``` diff --git a/medium/__init__.py b/medium/__init__.py index 0cdc79b..72b118b 100644 --- a/medium/__init__.py +++ b/medium/__init__.py @@ -7,84 +7,139 @@ import requests -BASE_PATH = "https://api.medium.com" +def _request(method, path, access_token, json=None, form_data=None, + files=None): + """Make a signed request to the given route.""" + url = "https://api.medium.com/v1" + path + headers = { + "Accept": "application/json", + "Accept-Charset": "utf-8", + "Authorization": "Bearer {}".format(access_token), + } + + resp = requests.request(method, url, json=json, data=form_data, + files=files, headers=headers) + json = resp.json() + if 200 <= resp.status_code < 300: + try: + return json["data"] + except KeyError: + return json -class Client(object): - """A client for the Medium OAuth2 REST API.""" + raise MediumError("API request failed", json) + +class Client(object): + """ + A client for the Medium OAuth2 REST API. + + >>> client = Client(application_id="MY_APPLICATION_ID", + ... application_secret="MY_APPLICATION_SECRET") + + To use the client, you first need to obtain an access token, which requires + an authorization code from the user. Send them to the URL given by + ``Client.get_authorization_url()`` and then have them enter their + authorization code to exchange it for an access token. + + >>> client.access_token + None + >>> redirect_url = "https://yoursite.com/callback/medium" + >>> authorize_url = client.get_authorization_url( + ... state="secretstate", + ... redirect_url=redirect_url, + ... scopes=["basicProfile", "publishPost"] + ... ) + >>> print 'Go to: {}'.format(authorize_url) + Go to: https://medium.com/m/oauth/authorize?scope=basicProfile%2Cpublish... + >>> print 'Copy the authorization code.' + Copy the authorization code. + >>> authorization_code = raw_input('Enter the authorization code here: ') + >>> client.exchange_authorization_code(authorization_code, redirect_url) + >>> client.access_token + ... + + The access token will expire after some time. To refresh it: + + >>> client.exchange_refresh_token() + + Once you have an access token, you can use the rest of the client's + methods. For example, to get the profile details of user identified by the + access token: + + >>> user = client.get_current_user() + + And to create a draft post: + + >>> post = client.create_post( + ... user_id=user["id"], + ... title="Title", + ... content="Content
", + ... content_format="html", publish_status="draft" + ... ) + + """ def __init__(self, application_id=None, application_secret=None, - access_token=None): + access_token=None, refresh_token=None): self.application_id = application_id self.application_secret = application_secret self.access_token = access_token + self.refresh_token = refresh_token def get_authorization_url(self, state, redirect_url, scopes): """Get a URL for users to authorize the application. - :param str state: A string that will be passed back to the redirect_url - :param str redirect_url: The URL to redirect after authorization - :param list scopes: The scopes to grant the application + :param str state: + A string that will be passed back to the redirect_url + :param str redirect_url: + The URL to redirect after authorization + :param list scopes: + The scopes to grant the application :returns: str """ - qs = { + return "https://medium.com/m/oauth/authorize?" + urlencode({ "client_id": self.application_id, - "scope": ",".join(scopes), - "state": state, "response_type": "code", "redirect_uri": redirect_url, - } + "scope": ",".join(scopes), + "state": state, + }) - return "https://medium.com/m/oauth/authorize?" + urlencode(qs) + def _get_tokens(self, **kwargs): + return _request('POST', '/tokens', self.access_token, **kwargs) def exchange_authorization_code(self, code, redirect_url): - """Exchange the authorization code for a long-lived access token, and - set the token on the current Client. - - :param str code: The code supplied to the redirect URL after a user - authorizes the application - :param str redirect_url: The same redirect URL used for authorizing - the application - :returns: A dictionary with the new authorizations :: - { - 'token_type': 'Bearer', - 'access_token': '...', - 'expires_at': 1449441560773, - 'refresh_token': '...', - 'scope': ['basicProfile', 'publishPost'] - } """ - data = { - "code": code, + Exchange the authorization code for a long-lived access token, and set + both the access token and refresh on the current Client. + + :param str code: + The code supplied to the redirect URL after a user authorizes the + application. + :param str redirect_url: + The same redirect URL used for authorizing the application. + """ + tokens = self._get_tokens(form_data={ "client_id": self.application_id, "client_secret": self.application_secret, + "code": code, "grant_type": "authorization_code", "redirect_uri": redirect_url, - } - return self._request_and_set_auth_code(data) - - def exchange_refresh_token(self, refresh_token): - """Exchange the supplied refresh token for a new access token, and - set the token on the current Client. + }) + self.access_token = tokens['access_token'] + self.refresh_token = tokens['refresh_token'] - :param str refresh_token: The refresh token, as provided by - ``exchange_authorization_code()`` - :returns: A dictionary with the new authorizations :: - { - 'token_type': 'Bearer', - 'access_token': '...', - 'expires_at': 1449441560773, - 'refresh_token': '...', - 'scope': ['basicProfile', 'publishPost'] - } + def exchange_refresh_token(self): + """ + Exchange the supplied refresh token for a new access token, and set the + token on the current Client. """ - data = { - "refresh_token": refresh_token, + self.access_token = self._get_tokens(form_data={ "client_id": self.application_id, "client_secret": self.application_secret, "grant_type": "refresh_token", - } - return self._request_and_set_auth_code(data) + "refresh_token": self.refresh_token, + })['access_token'] def get_current_user(self): """Fetch the data for the currently authenticated user. @@ -101,7 +156,7 @@ def get_current_user(self): 'name': 'Kyle Hardgrave' } """ - return self._request("GET", "/v1/me") + return _request("GET", "/me", self.access_token) def create_post(self, user_id, title, content, content_format, tags=None, canonical_url=None, publish_status=None, license=None): @@ -144,32 +199,34 @@ def create_post(self, user_id, title, content, content_format, tags=None, 'id': '55050649c95' } """ - data = { + json = { "title": title, "content": content, "contentFormat": content_format, } if tags is not None: - data["tags"] = tags + json["tags"] = tags if canonical_url is not None: - data["canonicalUrl"] = canonical_url + json["canonicalUrl"] = canonical_url if publish_status is not None: - data["publishStatus"] = publish_status + json["publishStatus"] = publish_status if license is not None: - data["license"] = license + json["license"] = license - path = "/v1/users/%s/posts" % user_id - return self._request("POST", path, json=data) + return _request("POST", "/users/{}/posts".format(user_id), + self.access_token, json=json) def upload_image(self, file_path, content_type): """Upload a local image to Medium for use in a post. Requires the ``uploadImage`` scope. - :param str file_path: The file path of the image - :param str content_type: The type of the image. Valid values are + :param str file_path: + The file path of the image + :param str content_type: + The type of the image. Valid values are ``image/jpeg``, ``image/png``, ``image/gif``, and ``image/tiff``. - :returns: A dictionary with the image data :: + :returns: A dictionary with the image data:: { 'url': 'https://cdn-images-1.medium.com/0*dlkfjalksdjfl.jpg', @@ -177,35 +234,9 @@ def upload_image(self, file_path, content_type): } """ with open(file_path, "rb") as f: - filename = basename(file_path) - files = {"image": (filename, f, content_type)} - return self._request("POST", "/v1/images", files=files) - - def _request_and_set_auth_code(self, data): - """Request an access token and set it on the current client.""" - result = self._request("POST", "/v1/tokens", form_data=data) - self.access_token = result["access_token"] - return result - - def _request(self, method, path, json=None, form_data=None, files=None): - """Make a signed request to the given route.""" - url = BASE_PATH + path - headers = { - "Accept": "application/json", - "Accept-Charset": "utf-8", - "Authorization": "Bearer %s" % self.access_token, - } - - resp = requests.request(method, url, json=json, data=form_data, - files=files, headers=headers) - json = resp.json() - if 200 <= resp.status_code < 300: - try: - return json["data"] - except KeyError: - return json - - raise MediumError("API request failed", json) + return _request("POST", "/images", self.access_token, files={ + "image": (basename(file_path), f, content_type) + }) class MediumError(Exception):