Publish Your Hashnode Articles with 3 Lines Python Code 🤔

Publish Your Hashnode Articles with 3 Lines Python Code 🤔

Simplify the process of publishing your articles on Hashnode by using a concise Python script.

Python is the primary programming language I use as a developer, and I find it truly enjoyable. It's great to be proficient in a language that I am comfortable with. Participating in hashnode hackathons adds an extra level of excitement as I get to create projects using python. From the previous mindsdb hackathon to the ongoing hashnode hackathon, the experience has been really thrilling.


The Ideađź’ˇ

Initially, my quest was to find a Python solution to interact with Hashnode. However, my search led me to a package that was built on the outdated version of Hashnode's API. Realizing this, I made up my mind to create a fresh package that would be compatible with the new GraphQL API.

After making up my mind, I eagerly dived into the hashnode graphql api playground to explore and grasp the workings of the new api. It didn't take long for me to realize that I needed to delve into the documentation to gather more information. Once I had a good understanding, I began crafting my code and envisioning how it would be put to use.


Hashnode-Py package 📦

My journey commenced with my efforts to establish a connection with the API using Python. By leveraging the power of the gql package in Python, I managed to submit a series of requests that exceeded my expectations. This sense of fulfillment propelled me forward, igniting my passion for further developing my package.

I've crafted a client file that serves as a comprehensive repository for all the data needed to connect with the API. This file not only facilitates seamless communication with the API but also allows for the smooth submission of API requests.

from gql import Client, gql
from gql.transport.requests import RequestsHTTPTransport

class HashnodeClient:
    def __init__(self, token: str):
        """
        Initializes the class with a token and sets up the client with the provided token for API requests.
        :param token: Str - the token used for authorization
        :return: None
        """
        if not token:
            raise ValueError("No token provided")
        self.token = token
        headers = {
            'Authorization': self.token
        }
        _transport = RequestsHTTPTransport(
            url='https://gql.hashnode.com/',
            headers=headers,
            use_json=True,
        )
        self.client = Client(
            transport=_transport,
            fetch_schema_from_transport=True
        )

Afterward, construct a method that effectively handles all the required API requests for the Hashnode GraphQL API.

    def fetch_data(self, query: str, variables: dict = None) -> dict:
        """
        Fetches data from the GraphQL API using the provided variables, headers, and query.
        :param variables: Dict - the variables used in the query
        :param query: Str - the query to be executed
        :return: Dict - the data returned from the query
        """
        if not variables:
            variables = {}

        response = self.client.execute(
            document=gql(query),
            variable_values=variables
        )

        return response

Subsequently, I initiated the development of a resources folder specifically designed to accommodate the classes of the Hashnode API models, along with their respective attributes and methods.

I have also set up a dedicated folder called "queries" where you can find all the queries and mutations for the API. This folder will help you easily locate and access the necessary information whenever you need it.

The Most Interesting Part 🤩

After incorporating some beneficial methods and attributes for every resource, along with their respective queries, I proceeded to seamlessly integrate all the mutations from the Hashnode GraphQL API into my code. This integration will provide users with the ability to effortlessly follow/unfollow other users, publish, update, and delete posts, as well as create webhooks. By expanding the capabilities of the API, I aim to enhance the overall user experience and empower them with more control over their interactions within the platform.

def toggle_follow(self, username: str):
        """
        Follows or unfollows a user based on the specified username and id.
        Args:
            username (str): The username of the user to follow or unfollow.
        """
        query = toggle_follow
        variables = {'username': username}
        data = self.client.fetch_data(query=query, variables=variables)
        status = data['toggleFollowUser']['user']['following']
        if status:
            return f'Successfully followed {username}'
        elif not status:
            return f'Successfully unfollowed {username}'

Even though the API's toggle follow mutation doesn't provide any status data when you follow or unfollow a user, I have implemented some checks in toggle_follow method to assist you in determining whether you are following or unfollowing the user.

def publish_post(self, title: str, content: str, tags_id: list = None, tags_slug: list[dict] = None,
                     subtitle: str = None, image_url: str = None, slug: str = None, origin_url: str = None,
                     disable_comments: bool = False, publish_as: str = None, series_id: str = None,
                     scheduled: bool = False, enable_table: bool = False, co_authors: list = None):
        """
        Publishes a new post to the specified publication.
        Args:
            title (str): The title of the post.
            content (str): The content of the post.
            tags_id (list): A list of tag IDs to associate with the post.
            tags_slug (list[dict]): a list of dictionaries containing tags slug and name.
                Example: [{'slug': 'tag1', 'name': 'Tag 1'}]
            subtitle (str, optional): The subtitle of the post. Defaults to None.
            image_url (str, optional): The URL of the image to use for the post. Defaults to None.
            slug (str, optional): The slug of the post. Defaults to None.
            origin_url (str, optional): The original URL of the post. Defaults to None.
            disable_comments (bool, optional): Whether to disable comments on the post. Defaults to False.
            publish_as (str, optional): The type of post to publish. Defaults to None.
            series_id (str, optional): The ID of the series to which the post belongs. Defaults to None.
            scheduled (bool, optional): Whether the post is scheduled. Defaults to False.
            enable_table (bool, optional): Whether to enable table of content mode. Defaults to False.
            co_authors (list, optional): A list of coauthor ids. Defaults to None.
        """
        tags = None

        if not tags_id and not tags_slug:
            raise ValueError('Either tags_id or tags_slug must be provided.')

        if tags_id:
            tags = [{'id': tag_id} for tag_id in tags_id]
        elif tags_slug:
            tags = tags_slug
        query = publish_post
        variables = {
            'title': title,
            'content': content,
            'tags': tags,
            'subtitle': subtitle,
            'publicationId': self.id,
            'imageUrl': image_url,
            'slug': slug,
            'originUrl': origin_url,
            'disableComments': disable_comments,
            'publishAs': publish_as,
            'seriesId': series_id,
            'settings': {
                'scheduled': scheduled,
                'enableTableOfContent': enable_table
            },
            'coAuthors': co_authors
        }

        data = self.client.fetch_data(query=query, variables=variables)
        return f'Successfully published with id: "{data["publishPost"]["post"]["id"]}"'

The publish_post method in publication.py allows you to publish an article from your code with just 3 required parameters. However, it is advisable to include the additional parameters for better functionality.

def publish_draft(self, draft_id: str) -> str:
        """
        Publishes a draft.
        Args:
            draft_id (str): The ID of the draft to publish.
        """
        query = publish_draft
        variables = {'draftId': draft_id}
        data = self.client.fetch_data(query=query, variables=variables)
        return f'Successfully published with id: "{data["publishDraft"]["post"]["id"]}"'

    def create_webhook(self, url: str, events: list[str], secret: str) -> Webhook:
        """
        Creates a webhook for the publication.
        Args:
            url (str): The URL of the webhook.
            events (list): The webhook will be triggered if any of the selected events occur.
                post_published, post_updated, post_deleted, static_page_published,
                static_page_edited, static_page_deleted.
            secret (str): The secret to use for the webhook.
        """
        for i in events:
            events.append(i.upper())
        query = create_webhook
        variables = {'publicationId': self.id, 'url': url, 'events': events, 'secret': secret}
        data = self.client.fetch_data(query=query, variables=variables)
        return Webhook(data, self.client)

You also have the option to publish your saved drafts using publish_draft method and create webhooks for your published articles using create_webhook.

Testing the code âś…

I've incorporated a few unittests to assist me in identifying any issues within my code. Naturally, I haven't tested every single method, but I've covered a good portion of them.

import unittest
from hashnode_py.client import HashnodeClient


class MutationsTest(unittest.TestCase):
    def setUp(self):
        """
        Set up the test environment by initializing the HashnodeClient with a specific token.
        """
        self.client = HashnodeClient(token="<YOUR_TOKEN_HERE>")

    def test_publish_draft(self):
        """
        Test the publish_draft mutation
        """
        publication = self.client.get_publication("talaat.hashnode.dev")
        draft = publication.get_drafts()[0]
        post = publication.publish_draft(draft_id=draft.id)
        self.assertIsNotNone(post)


if __name__ == '__main__':
    unittest.main()

How to use itâť“

To install this package, make sure that the required packages are already installed:

pip install gql requests requests-toolbelt

and the install hashnode-py package with:

pip install hashnode-py

You can easily define a client with your Hashnode developer token and start interacting with your Hashnode data using Python.

from hashnode_py import HashnodeClient

client = HashnodeClient("<YOUR_TOKEN_HERE>")
publication = client.get_publication("<YOUR_BLOG_DOMAIN_NAME>")
print(publication.publish_post(
            title="this is test title",
            content="this is a test content",
            tags_slug=[{"slug": "test", "name": "Test"}]
        ))

The message you will see after successful publishing is:

"Successfully published with id: <YOUR_POST_ID>" đź’Ż


How does this package serve a purposeâť“

  • This package is designed to seamlessly integrate with numerous Python applications, making it an excellent tool to spread awareness about hashnode. Given the vast size of the Python community, you can easily incorporate it into popular Python apps such as MindsDB, Datadog, and countless others.

  • This package enables you to easily access your data on hashnode. With it, you can apply data analytics techniques to enhance your performance and writing style. Additionally, it empowers you to utilize machine learning techniques on this data to predict future posts you can write about.

  • If you are a Python Developer With the help of this tool, you can easily publish your articles and thoughts on Hashnode. Imagine the convenience of writing your ideas in code and having them automatically posted. It's that simple!


I would like to express my gratitude to hashnode for organizing such amazing hackathons and fostering a wonderful community. Feel free to share your thoughts about the article in the comments section below. I'm eagerly looking forward to hearing from you 🤠.

Â