Making a Python wrapper for IPFS

Making a Python wrapper for IPFS

How I created ipfslib

ยท

5 min read

I've wanted to publish my first article about an encrypted, decentralized chat app running on IPFS. I had already built a private IPFS library, but I wanted to use a public one for the article. I tried out two different public libraries. One of them simply didn't work and the other one was deprecated.
So here I am rebuilding my library publically!

Step 1 - Preparing

Reading the documentation

The last time I've build this, it wasn't in the most elegant way. I used the CLI Tools and extracted the data I needed with regex. I have done many unconventional things, but I was too lazy to read the documentation and wanted to get a mini-project done quickly to show my friends. As I am building this publically now, I will read the IPFS documentation (IPFS Kubo RPC API).

What do I want to achieve with this library?

As far as Version 1 goes, I only want to implement basic functionality, so I can finish my first intended article. In particular, I want to be able to do at least the following things:

  • Add files to IPFS

  • Get files from IPFS

  • Remove files from IPFS

  • Generate IPNS Keys

  • List IPNS Keys

  • Update IPNS values

Creating the folder structure

At this point, I have to create the root folder. This will be the name of my library. I will call it ipfslib, so it's easier to confuse with ipfsapi. Every module has it's own file, this way it is less cluttered and easier to update afterwards.

    ipfslib
    โ”‚   connect.py
    โ”‚   __init__.py
    โ”‚
    โ”œโ”€โ”€โ”€IPFS
    โ”‚   โ”‚   add.py
    โ”‚   โ”‚   cat.py
    โ”‚   โ”‚   get.py
    โ”‚   โ”‚   rem.py
    โ”‚   โ”‚   resolve.py
    โ”‚   โ”‚   __init__.py
    โ”‚
    โ””โ”€โ”€โ”€Key
        โ”‚   generate.py
        โ”‚   list.py
        โ”‚   publish.py
        โ”‚   rename.py
        โ”‚   __init__.py

I included everything I want to have in my first version. This is all I need for my next project.

Creating the Connector

I want the user to be able to specify where the API-endpoint is, instead of assuming standard values. Basically this is all, what connect.py does, it stores the IP-address and the port of the API-endpoint:

class Connect:
    def __init__(self, ip_address, port):
        self.endpoint = str(ip_address) + ":" + str(port)

But I included some checks to check if the API is responding and I included standard parameters, so users could simply write ipfslib.Connect(), instead of typing ipfslib.Connect('127.0.0.1', 5001).

# ipfslib/connect.py
import requests

# Sets up the API-Connector
class Connect:
    def __init__(self, ip_address="localhost", port=5001):

        # Check if port has the right format
        if str(port).isnumeric() == False:
            raise TypeError("The given port is not numeric")
        elif int(port) <= 0 or int(port) >= 65536:
            raise ValueError("Port number has to be between 1 and 65535")

        # Saves API Endpoint if port checks are passed
        self.endpoint = str(ip_address) + ":" + str(port)

        # Checks if the API is responding
        response = requests.post('http://{endpoint}/api/v0/bitswap/stat'.format(endpoint=self.endpoint))
        if response.status_code != 200:
            raise Exception("The given endpoint isn't working as intended")

Step 2 - Wrapping those APIs up๐Ÿ˜‹

You don't need to be a professional programmer to do this stuff - even I can do it.

Let's say, I want to add a file to IPFS. The way to do it is quite simple, because the IPFS documentation has CURL examples. Here is a quick overview of what I did it:

  1. Go to the documentation, where adding files is specified

  2. Copy the provided CURL example

  3. Paste it at curlconverter.com

  4. Integrate the Python Code into the function

  5. Read out the JSON response and extract relevant values

Example: Generating IPNS key

The CURL example in the IPFS documentation:

curl -X POST "http://127.0.0.1:5001/api/v0/key/gen?arg=<name>&type=ed25519&size=<value>&ipns-base=base36"

Removing unnecessary things and formatting:

curl -X POST "http://{endpoint}/api/v0/key/gen?arg="

Using curlconverter.com:

import requests

params = {
    'arg': '',
}

response = requests.post('http://{endpoint}/api/v0/key/gen', params=params)

Implementing it into a function, which takes the JSON response and extracts the ipns_name of the newly created key:

# ipfslib/Key/generate.py
import json
import requests

def generate(api, key_name):
    params = {
        'arg': key_name,
    }
    response = requests.post('http://{endpoint}/api/v0/key/gen'.format(api.endpoint), params=params)
    ipns_name = json.loads(response.text)["Id"]
    return ipns_name

I did this for every feature I wanted to have in my library. Some were more complex than others, I showed you the most simple function there was for this example.
Other modules had more complex JSON responses or more functionality.

Including everything in __init__.py

I don't want the user to import every module separately, so I'll include them in those nice little __init__.py files.

# ipfslib/__init__.py
from ipfslib.connect import Connect
from ipfslib import Key
from ipfslib import IPFS
# ipfslib/IPFS/__init__.py
from ipfslib.IPFS.add import add
from ipfslib.IPFS.cat import cat
from ipfslib.IPFS.get import get
from ipfslib.IPFS.rem import rem
from ipfslib.IPFS.resolve import resolve
# ipfslib/Key/__init__.py
from ipfslib.Key.generate import generate
from ipfslib.Key.list import list
from ipfslib.Key.publish import publish
from ipfslib.Key.rename import rename

Step 3 - Publishing

Surprisingly this was taking longer than actually programming because it is my first time actually doing this last step of publishing my project. In future projects, this won't take that long anymore, because I've learned everything important now.
Like how to structure a setup.py file or how to use twine, which is fairly easy.

Writing documentation

I explained how to use each module quickly in the README.md file. There is nothing too much to explain here. I had to look at how Markup works, but it was a simple process as a whole.

Creating the setup file

The setup.py file ended up looking like this. I had to change the version from 0.1 to 0.1.0, because I messed up the markup file on my first try uploading. I took inspiration from here to fix the problem with my project description.

from setuptools import setup, find_packages
import codecs
import os

here = os.path.abspath(os.path.dirname(__file__))

with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
    long_description = "\n" + fh.read()

setup(
    name=           "ipfslib",
    version=        "0.1.0",
    author=         "Christian Remboldt",
    author_email=   "remboldt@proton.me",
    description=    "IPFS Library for Python",
    long_description_content_type="text/markdown",
    long_description=long_description,
    packages=find_packages(),
    install_requires=[],
    keywords=['python', 'ipfs', 'api', 'decentral', 'networking', 'ipns'],
    classifiers=[
        "Programming Language :: Python :: 3"
    ]
)

Uploading

First I had to build the setup.py file

py setup.py sdist

After python had created the dist folder, I could upload its contents to PyPi with twine.

py -m pip install twine
py -m twine upload dist/*

AND DONE!

Note

I uploaded some usage examples on GitHub:
https://github.com/remboldt/ipfslib/tree/main/examples Have a nice day!

PyPi Project Page: https://pypi.org/project/ipfslib/
GitHub Repository: https://github.com/remboldt/ipfslib/

ย