This module is as close as possible to the barebone Medium API but written in python instead of GET/POST commands. Not all the kinds requests are implemented, only the ones that are useful to post an article or an image

base_request[source]

base_request()

Fetch User Data

The basic user data can be requested by passing the Medium Integration token via a GET request. The Medium Integration Token needs to be requested from the writer's Medium profile page. For now, this token should be stored as an environment variable under the user's $HOME/.profile file:

export MEDIUM_TOKEN=<the-token>

I may consider using keyring to store the token locally as that may more user friendly.

auth_header[source]

auth_header(token=None)

fetch_user_data[source]

fetch_user_data()

a = fetch_user_data()
a.json()
{'data': {'id': '1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656',
  'username': 'lucha6',
  'name': 'Luis Chaves',
  'url': 'https://medium.com/@lucha6',
  'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/0*rM2cEh8f6ZQMOAZK.jpg'}}

Which is equivalent to the curl alternative:

!curl -s -H "Authorization: Bearer $MEDIUM_TOKEN" https://api.medium.com/v1/me | jq
{
  "data": {
    "id": "1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656",
    "username": "lucha6",
    "name": "Luis Chaves",
    "url": "https://medium.com/@lucha6",
    "imageUrl": "https://cdn-images-1.medium.com/fit/c/400/400/0*rM2cEh8f6ZQMOAZK.jpg"
  }
}
assert 'data' in fetch_user_data().json().keys()

Get User ID

get_user_id[source]

get_user_id()

get_user_id()
'1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656'

Fetch User Publications

We can also request the user's publications. In Medium's definition a publication is not an article but rather an editorial-like group under which articles are written (e.g. Towards Data Science, Elemental AI...)

fetch_publications[source]

fetch_publications()

fetch_publications().json()
{'data': [{'id': '7f60cf5620c9',
   'name': 'Towards Data Science',
   'description': 'Your home for data science. A Medium publication sharing concepts, ideas and codes.',
   'url': 'https://medium.com/towards-data-science',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*hVxgUA6kP-PgL5TJjuyePg.png'},
  {'id': '4b3a1ed4f11c',
   'name': 'JavaScript in Plain English',
   'description': 'New JavaScript and Web Development articles every day.',
   'url': 'https://medium.com/javascript-in-plain-english',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*iETPsI-y6GMmx-AJEQRBnw@2x.png'},
  {'id': '261e46dce6ca',
   'name': '<pretty/code>',
   'description': 'Topics centered around Ruby, Rails, Coffeescript, Vim, Tmux and Productivity.',
   'url': 'https://medium.com/raise-coffee',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*pLo2lxSseBKg09Nc_1EOlw.png'},
  {'id': '3a8144eabfe3',
   'name': 'HackerNoon.com',
   'description': 'Elijah McClain, George Floyd, Eric Garner, Breonna Taylor, Ahmaud Arbery, Michael Brown, Oscar Grant, Atatiana Jefferson, Tamir Rice, Bettie Jones, Botham Jean',
   'url': 'https://medium.com/hackernoon',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*76XiKOa05Yya6_CdYX8pVg.jpeg'},
  {'id': '5517fd7b58a6',
   'name': 'Level Up Coding',
   'description': 'Coding tutorials and news. The developer homepage gitconnected.com',
   'url': 'https://medium.com/gitconnected',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*5D9oYBd58pyjMkV_5-zXXQ.jpeg'},
  {'id': '8d6b8a439e32',
   'name': 'Elemental',
   'description': 'Your life, sourced by science. A publication from Medium about health and wellness.',
   'url': 'https://medium.com/elemental-by-medium',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*GhG8ZeoE0TGfCHwL9SCrfw.png'},
  {'id': '98111c9905da',
   'name': 'Towards AI',
   'description': 'Towards AI is the world’s leading multidisciplinary science publication. Towards AI publishes the best of tech, science, and engineering. Read by thought-leaders and decision-makers around the world.',
   'url': 'https://medium.com/towards-artificial-intelligence',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*JyIThO-cLjlChQLb6kSlVQ.png'},
  {'id': 'b7e45b22fec3',
   'name': 'Creators Hub',
   'description': 'The Creators Hub is your source of ongoing education and inspiration to help your presence on Medium grow and support your creative practice. You’ll find tips on the craft of writing, spotlights on thinkers across the platform, and advice from Medium editors and fellow writers.',
   'url': 'https://medium.com/creators-hub',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*8Zti0Ox8AfGECDO_O1Ifug.png'},
  {'id': 'd0b105d10f0a',
   'name': 'Better Programming',
   'description': 'Advice for programmers.',
   'url': 'https://medium.com/better-programming',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*TyRLQdZO7NdPATwSeut8gg.png'},
  {'id': 'f5105b08f43a',
   'name': 'DailyJS',
   'description': 'JavaScript news and opinion.',
   'url': 'https://medium.com/dailyjs',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*3RTyL2e-UvYez9Qo4YuFiA.png'},
  {'id': 'f5af2b715248',
   'name': 'The Startup',
   'description': 'Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +780K followers.',
   'url': 'https://medium.com/swlh',
   'imageUrl': 'https://cdn-images-1.medium.com/fit/c/400/400/1*pKOfOAOvx-fWzfITATgGRg.jpeg'}]}
assert 'data' in fetch_publications().json().keys()

Post an Article

As easily an article can be submitted. Many options are available which can be explored in the Medium API official docs. The possible parameters and parameter values are as follow:

Parameter Type Required? Description
title string required The title of the post. Note that this title is used for SEO and when rendering the post as a listing, but will not appear in the actual post—for that, the title must be specified in the content field as well. Titles longer than 100 characters will be ignored. In that case, a title will be synthesized from the first content in the post when it is published.
contentFormat string required The format of the "content" field. There are two valid values, "html", and "markdown"
content string required The body of the post, in a valid, semantic, HTML fragment, or Markdown. Further markups may be supported in the future. For a full list of accepted HTML tags, see here. If you want your title to appear on the post page, you must also include it as part of the post content.
tags string array optional Tags to classify the post. Only the first three will be used. Tags longer than 25 characters will be ignored.
canonicalUrl string optional The original home of this content, if it was originally published elsewhere.
publishStatus enum optional The status of the post. Valid values are “public”, “draft”, or “unlisted”. The default is “public”.
license enum optional The license of the post. Valid values are “all-rights-reserved”, “cc-40-by”, “cc-40-by-sa”, “cc-40-by-nd”, “cc-40-by-nc”, “cc-40-by-nc-nd”, “cc-40-by-nc-sa”, “cc-40-zero”, “public-domain”. The default is “all-rights-reserved”.
notifyFollowers bool optional Whether to notifyFollowers that the user has published.

The default publishStatus for posting articles will always be set to 'draft' because that is how I would always like to use this API.

post_article[source]

post_article(title, content, contentFormat='markdown', tags=None, canonicalUrl=None, publishStatus='draft', license=None, notifyFollowers=False)

Which is equivalent to:

This command posts an article via a simple POST request. If the article is correctly submitted a JSON response like the below will be returned

Posting a string of text

my_post = post_article('Test from nb',
             '# Markdown title \n\n markdown text')
my_post.json()
{'data': {'id': '4955a2343f8c',
  'title': 'Test from nb',
  'authorId': '1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656',
  'url': 'https://medium.com/@lucha6/4955a2343f8c',
  'canonicalUrl': '',
  'publishStatus': 'draft',
  'license': '',
  'licenseUrl': 'https://policy.medium.com/medium-terms-of-service-9db0094a1e0f',
  'tags': []}}

The request should return the HTTP 201 code

Posting a text document

my_post_from_file = post_article('Test from file',open('../samples/LEARNING.md', 'rb').read())
my_post_from_file.json()
{'data': {'id': '8ffa0d606ea5',
  'title': 'Test from file',
  'authorId': '1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656',
  'url': 'https://medium.com/@lucha6/8ffa0d606ea5',
  'canonicalUrl': '',
  'publishStatus': 'draft',
  'license': '',
  'licenseUrl': 'https://policy.medium.com/medium-terms-of-service-9db0094a1e0f',
  'tags': []}}
assert my_post_from_file.ok
assert my_post_from_file.status_code == 201 

Post/Upload an image

post_image[source]

post_image(filename=None, img=None)

filename: needs to be a valid image file path supported my Medium img: can be a binary image representation

Upload image from file

my_img = post_image('../samples/github-logo.png')
my_img.json()
{'data': {'url': 'https://cdn-images-1.medium.com/proxy/1*sV7tva-728oySeOUL0-vOw.png',
  'md5': 'sV7tva-728oySeOUL0-vOw'}}
assert my_img.ok
assert my_img.status_code == 201

Upload image as byte stream

from io import BytesIO
img = BytesIO(open('../samples/github-logo.png', 'rb').read()).getvalue()
print(f"Which is some sort of byte stream: like {img[:10]}")
Which is some sort of byte stream: like b'\x89PNG\r\n\x1a\n\x00\x00'
image_from_bytes = post_image(filename = 'github-logo.png', img = img).json()
image_from_bytes
{'data': {'url': 'https://cdn-images-1.medium.com/proxy/1*sV7tva-728oySeOUL0-vOw.png',
  'md5': 'sV7tva-728oySeOUL0-vOw'}}

It may not become apparent why this functionality is useful right now. Being able to upload a file as a binary stream is actually really useful because we can avoid saving the images to memory and upload them directly to Medium, the nbconvert modules that we will be using later represent images in such intermediate states

Which as a curl call would be:

!curl -X POST https://api.medium.com/v1/images \
	-H "Authorization: Bearer $MEDIUM_TOKEN" \
	-F 'name="image"; filename="../samples/github-logo.png" ; type="image/png";' \
	-F 'image=@../samples/github-logo.png' | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 51237  100   116  100 51121     65  28800  0:00:01  0:00:01 --:--:-- 28865
{
  "data": {
    "url": "https://cdn-images-1.medium.com/proxy/1*sV7tva-728oySeOUL0-vOw.png",
    "md5": "sV7tva-728oySeOUL0-vOw"
  }
}

Posting an article with an image

Post with online image links

It turns out that uploading a file with an online image may be easier

with open('../samples/test-offline-image.md', 'rb') as article:
    my_post_from_file_with_online_image = post_article(
        'Test from file with online image',
        article.read())
my_post_from_file_with_online_image.json()
{'data': {'id': '148cba4f9155',
  'title': 'Test from file with online image',
  'authorId': '1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656',
  'url': 'https://medium.com/@lucha6/148cba4f9155',
  'canonicalUrl': '',
  'publishStatus': 'draft',
  'license': '',
  'licenseUrl': 'https://policy.medium.com/medium-terms-of-service-9db0094a1e0f',
  'tags': []}}

For those reading this code above, there is no way for me to show that the draft was posted succesfully and that the image is displayed correctly as the draft is posted to my account, but trust me it is posted correctly. Where as obtaining a correct JSON back is a good sign that the article was posted succesfully, the posted draft still needs to be verified and potentially modified.

with open('../samples/test-offline-image.md', 'rb') as article:
    my_post_from_file_with_offline_image = post_article(
        'Test from file with offline image',
        article.read())
my_post_from_file_with_offline_image.json()
{'data': {'id': '9e2aeeeaaf12',
  'title': 'Test from file with offline image',
  'authorId': '1e344db7dfd2a8efa9698a758030e05e28ff4c396f1f87f003704e0a8a80b9656',
  'url': 'https://medium.com/@lucha6/9e2aeeeaaf12',
  'canonicalUrl': '',
  'publishStatus': 'draft',
  'license': '',
  'licenseUrl': 'https://policy.medium.com/medium-terms-of-service-9db0094a1e0f',
  'tags': []}}

In this case, we referred to a local image in our markdown document as opposed to one found oneline. When we post an article with references to offline/local images, the medium Markdown renderer won't recognise the path to those images and will faily to display the image. What would need to be done in this case is to get the local image paths, upload them with the post_image() function and then replace the local path reference by the one in the Medium DB.