Reading Hashnode articles on Shell/Terminal

Reading Hashnode articles on Shell/Terminal

Using Hashnode Public APIs and Python Programming Language

I tried and joined Hashnode couple of months ago and i am really in love with this awesome platform. I was visiting the Hashnode public APIs to create mobile application for https://djangotherightway.com which is hosted on hashnode as well. Upon visiting and playing with the Hashnode Public APIs i created a mobile application which i am planning to open source soon. But for this article/tutorial i will be using same public APIs to create a simple console based Python script/application to read articles on terminal.

I will be trying to divide up this article into multiple sections so that it will help you understand

  1. Hashnode Public APIs
  2. Consuming external APIs with Python
  3. Creating a real world Python Application

Hashnode Public APIs

You can find Hashnode Public APIs here at : https://api.hashnode.com/. The best part is it consist of a playground where you can test GraphQL API easily in the browser. Upon playing with the APIs i found two useful APIs useful for our requirement

  • API to fetch post list of a user Query looks like:
query {
      user(username: "pydj") {
        username,
        name,
        tagline,
        publicationDomain,
        publication{
            title,
            meta,
            posts(page:0){
                slug,
                title,
                brief
            }
        }
      }
    }

Try this in your browser to see the response:

hashnode-list-all-post-api.png

  • API to fetch detail of a post Query Looks like:
query{
        post(slug:"django-setup-architecture-and-hello-world",
            hostname:"djangotherightway.com")
        {
            dateAdded,
            content
        }
}

Try this in your browser to see the response:

hashnode-post-detail-api.png

You can always change the query and get more details on the response, but for our requirements the query i just mentioned above would be enough.

let's start Python integration

First of all, before creating a python file we will use a simple library called requests to query the Hashnode api Install it using pip.

pip install requests

Learn more about requests library from https://docs.python-requests.org/en/master/

  • Base function to request Hashnode API This python function will take Query as an argument and return the response
def request_hashnode_dot_com(query):
    return requests.post("https://api.hashnode.com/", 
                         json={'query': query})
  • Craft query for Post list For our requirement, Post list Query would basically take two dynamic parameters username and page number. So let's create a python functions which takes username and page as an argument
def create_query_for_post_list(username, page):
    return """{
      user(username: "%s") {
        username,
        name,
        tagline,
        publicationDomain,
        publication{
            title,
            meta,
            posts(page:%d){
                slug,
                title,
                brief
            }
        }
      }
    }""" % (username, page)
  • Craft query for post detail For our requirement, Post detail Query would consist of two dynamic arguments post slug and hostname. Don't worry, we will get slug and hostname from the Post List API and we will use it here
def create_query_for_post_detail(slug, hostname):
    return """{
        post(slug:"%s",hostname:"%s"){
            dateAdded,
            content
        }
    }""" % (slug, hostname)
  • Creating a plain text from HTML The post detail Query will return us HTML version of our post on hashnode. Its difficult to parse the HTML and show it on the terminal so we will be using python native functions to basically parse the HTML to normal Text.So, lets create up a python function which takes HTML as an input and returns normal text which can be printed out in the terminal/console
def create_plain_text_from_html(html):
    from html.parser import HTMLParser

    class _CustomHTMLParser(HTMLParser):
        normal_text = ""

        def handle_data(self, data):
            self.normal_text += data

    f = _CustomHTMLParser()
    f.feed(html)
    return f.normal_text

We could have used the markdown format of your Post provided by the Hashnode post detail Query, but we used HTML version for simplicity and easy parsing through python internal functions

  • Creating our main code
def show_post_detail(article, publication_domain):
    q = create_query_for_post_detail(
        article['slug'], publication_domain
    )
    post_detail_reponse = request_hashnode_dot_com(q)
    if post_detail_reponse.status_code == 200:
        content = post_detail_reponse.json()['data']['post']['content']
        date_added = post_detail_reponse.json()['data']['post']['dateAdded']
        print(article['title'].upper())
        print("Added on:", date_added)
        print(create_plain_text_from_html(content))
    else:
        print("Error")
        exit(1)


def main():
    import sys
    username = sys.argv[1] if len(sys.argv) == 2 else "pydj"
    current_page = 0
    articles = {}

    while True:
        query = create_query_for_post_list(username, current_page)
        response = request_hashnode_dot_com(query)
        if response.status_code == 200:
            if not response.json()['data']["user"]["username"]:
                print("Invalid User")
                exit(1)
            re_data = response.json()['data']["user"]
            print("Welcome to:", re_data['publication']['title'])
            print("----List of Articles----")
            if not re_data['publication']['posts']:
                print("***No Articles Available, try changing page number***")
            else:
                for index, article in enumerate(re_data['publication']['posts'], 1):
                    articles[index] = article
                    print(f"[{index}]", article['title'])

            print("Current Page:", current_page + 1)
            print("-----------------")
            print(f"Type: n for next page\n"
                  f"      p for previous page\n"
                  f"      1-{len(articles)} for article detail")
            print("-----------------")

            user_input = input("Enter your input: ")

            valid_inputs = ["n", "p"] + [str(i) for i in articles.keys()]
            if user_input not in valid_inputs:
                print("Invalid choice")
                exit(1)
            if user_input == "n":
                current_page += 1
                continue
            if user_input == "p":
                if current_page == 0:
                    print("Cannot go to previous page")
                    exit(1)
                else:
                    current_page -= 1
                    continue

            show_post_detail(articles[int(user_input)], re_data['publicationDomain'])
            break

        else:
            print("error")
            exit(1)

Here to understand python and its ways, i created two functions main and show_post_detail. Actually show_post_detail was not important and on the other hand main function looks so messy and long.

The real thing i wanted to show you on the main function is to make you feel functions and code splits are important because according to my experience in software engineering Code Readability/Reusability is the most important factor, So feel free to play and work around it to make it more readable.

But, here i will try to write some steps that will make you feel easy to understand the messy long main function

  1. Takes username from CLI arguments python hn.py pydj
  2. prepares and runs query to grab posts of the supplied username
  3. Displays list of blog post [paginated data]
  4. creates up a simple menu bar
  5. Pagination functionality
  6. Post detail functionality

How does this look ?

Tested on

  • Python 3.9.1
  • requests==2.26.0

hashnode-terminal.png

Complete source code

Find the complete source code at: https://github.com/shrawanx/hashnode-terminal