[PYTHON] Create a (simple) REST server

Introduction

REST can be conveniently used for data communication between the server and the client. Regarding the usage of REST, there is information on how to create a client in various languages and frameworks. However, on the server side, there is information such as how to build with "JsonServer" or Apache + PHP as a mock that can be easily prepared, but it is a method of accepting inconvenience or taking time and effort. (It will be as far as I can find.)

This time, I tried to build a server that is in the middle of the above, which allows you to freely set services and easily build with one script. (I don't want it to be robust enough to be published on the WEB, but the purpose is to build my own Raspberry Pi service.)

Source code

It's a live resource code.

restserver.py


#!/usr/bin/env python3

import http.server
import json
import threading
import sys,os
import time

class RestHandler(http.server.BaseHTTPRequestHandler):
    def do_OPTIONS(self):
        #Supports preflight request
        print( "options" )
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
        self.send_header('Access-Control-Allow-Headers', '*')
        self.end_headers()

    def do_POST(self):
        print( "post" )
        local_path = self.path.strip("/").split("/")
        #Get request
        content_len  = int(self.headers.get("content-length"))
        body = json.loads(self.rfile.read(content_len).decode('utf-8'))

        #Response processing
        if( local_path[0] == "dat" ):
            if(os.path.getsize('./dat.json')):
              with open('./dat.json', 'r') as json_open:
                json_load = json.load(json_open)

              json_load.update(**body)
              json_wraite = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
            else:
              json_wraite = json.dumps(body, sort_keys=False, indent=4, ensure_ascii=False)

            with open('./dat.json', 'w') as json_open:
                json_open.write(json_wraite)

            self.send_response(200)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
            self.send_header('Access-Control-Allow-Headers', '*')
            self.end_headers()
            return
        else:
            print( "no" )
            print( self.path )
            return

    def do_GET(self):
        print( "get" )
        local_path = self.path.strip("/").split("/")
        #Response processing
        if( local_path[0] == "dat" ):
            print( "dat" )
            if(os.path.getsize('./dat.json')):
              with open('./dat.json', 'r') as json_open:
                json_load = json.load(json_open)

            else:
              json_load = {}

            self.send_response(200)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
            self.send_header('Access-Control-Allow-Headers', '*')
            self.send_header('Content-type', 'application/json;charset=utf-8')
            self.end_headers()
            body_json = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False) 
            self.wfile.write(body_json.encode("utf-8"))
            return
        else:
            print( "no" )
            print( self.path )
            return

    def do_DELETE(self):
        print( "delete" )
        local_path = self.path.strip("/").split("/")
        if( local_path[0] == "dat" ):
            print( "dat" )

            with open('./dat.json', 'w') as file_open:
                pass

            self.send_response(200)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
            self.send_header('Access-Control-Allow-Headers', '*')
            self.end_headers()
            return
        else:
            print( "no" )
            print( self.path )
            return

def rest_server(port):
    httpd_rest = http.server.ThreadingHTTPServer(("", port), RestHandler)
    httpd_rest.serve_forever()


def main():
    #Server startup
    port_rest  = 3333
    try:
        t1 = threading.Thread(target=rest_server,  args=(port_rest,),  daemon = True)

        t1.start()

        while True: time.sleep(1)

    except (KeyboardInterrupt, SystemExit):
        print("exit")
        sys.exit()

if __name__ == "__main__":
  main()

The HTTP server is set up on port 3333 by doing REST parsing with do_OPTIONS / do_POST / do_GET / do_DELETE of Python's BaseHTTPRequestHandler. The operation is simple, it updates (POST), gets the contents (GET), and initializes (DELETE) the "dat.json" file in the current directory.

OPTIONS? PUT?

REST usually accepts the following four types of requests.

It should be created according to this, but I decided not to prepare PUT because POST can be used as a substitute.

For "OPTIONS" that are not included in the 4 types, depending on the browser specifications, an OPTIONS request will be issued prior to the POST request. The "Access-Control-Allow-Methods" included in the header of the response to this OPTIONS request describes the methods that the server can handle, and if POST is included, the POST request is issued following the OPTIONS request. I will. This operation is called preflight, and it is a browser operation, so there are workarounds such as requesting from the server side, but if you prepare your own server, it is easier to support OPTIONS request, so make it compatible. I did.

Function description

I will explain each volume. do_OPTIONS

do_OPTIONS


    def do_OPTIONS(self):
        #Supports preflight request
        print( "options" )
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
        self.send_header('Access-Control-Allow-Headers', '*')
        self.end_headers()

Create a response to the OPTIONS request described in the previous section. It simply returns a response.

do_POST

do_POST


    def do_POST(self):
        print( "post" )
        local_path = self.path.strip("/").split("/")
        #Get request
        content_len  = int(self.headers.get("content-length"))
        body = json.loads(self.rfile.read(content_len).decode('utf-8'))

        #Response processing
        if( local_path[0] == "dat" ):
            if(os.path.getsize('./dat.json')):
              with open('./dat.json', 'r') as json_open:
                json_load = json.load(json_open)

              json_load.update(**body)
              json_wraite = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
            else:
              json_wraite = json.dumps(body, sort_keys=False, indent=4, ensure_ascii=False)

            with open('./dat.json', 'w') as json_open:
                json_open.write(json_wraite)

            self.send_response(200)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
            self.send_header('Access-Control-Allow-Headers', '*')
            self.end_headers()
            return
        else:
            print( "no" )
            print( self.path )
            return

The POST process writes the JSON data received in the POST request to a file. We will follow the process in order from the top.

local_path = self.path.strip("/").split("/")

self.path contains the URL data of the request. Divide this by "/" and keep it in the array. For example, if the request is "servername / aaa / bbb / ccc /", the local_path will be as follows. local_path[0]='aaa' local_path[1]='bbb' local_path[2]='ccc'

content_len  = int(self.headers.get("content-length"))
body = json.loads(self.rfile.read(content_len).decode('utf-8'))

It is a process to parse the json data received along with the request and store it in the dictionary type data.

#Response processing
if( local_path[0] == "dat" ):
    if(os.path.getsize('./dat.json')):
      with open('./dat.json', 'r') as json_open:
        json_load = json.load(json_open)

      json_load.update(**body)
      json_wraite = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False)
    else:
      json_wraite = json.dumps(body, sort_keys=False, indent=4, ensure_ascii=False)

    with open('./dat.json', 'w') as json_open:
        json_open.write(json_wraite)

    self.send_response(200)
    self.send_header('Access-Control-Allow-Origin', '*')
    self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
    self.send_header('Access-Control-Allow-Headers', '*')
    self.end_headers()
    return

In the next stage, check local_path [0] and it will be the API definition when it is "dat". In the sample, only local_path [0] is used, but the API will be defined by checking local_path [1] and local_path [2] in sequence. The rest are simple file operations. The reason for checking the file size first is that if you load an empty file in json.load (), an error will occur, so if the file is empty, the received data will be written to the file as it is. (File operations around here are verbose and muddy.)

The part that creates the final response is the same as OPTIONS and does not consider error handling.

else:
    print( "no" )
    print( self.path )
    return

This is when the URL is unexpected. Normally, you should return a 404 error, but since it is not a public service, it is boldly omitted.

do_GET

do_GET


    def do_GET(self):
        print( "get" )
        local_path = self.path.strip("/").split("/")
        #Response processing
        if( local_path[0] == "dat" ):
            print( "dat" )
            if(os.path.getsize('./dat.json')):
              with open('./dat.json', 'r') as json_open:
                json_load = json.load(json_open)

            else:
              json_load = {}

            self.send_response(200)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
            self.send_header('Access-Control-Allow-Headers', '*')
            self.send_header('Content-type', 'application/json;charset=utf-8')
            self.end_headers()
            body_json = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False) 
            self.wfile.write(body_json.encode("utf-8"))
            return
        else:
            print( "no" )
            print( self.path )
            return

The basic part is the same as POST. It eliminates the JSON data acquisition from the request, reads the file, and puts the data in the response.

self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
self.send_header('Access-Control-Allow-Headers', '*')
self.send_header('Content-type', 'application/json;charset=utf-8')
self.end_headers()
body_json = json.dumps(json_load, sort_keys=False, indent=4, ensure_ascii=False) 
self.wfile.write(body_json.encode("utf-8"))

The difference from POST is that Content-type is added to the header and data is written to the body part.

do_DELETE

do_DELETE


    def do_DELETE(self):
        print( "delete" )
        local_path = self.path.strip("/").split("/")
        if( local_path[0] == "dat" ):
            print( "dat" )

            with open('./dat.json', 'w') as file_open:
                pass

            self.send_response(200)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
            self.send_header('Access-Control-Allow-Headers', '*')
            self.end_headers()
            return
        else:
            print( "no" )
            print( self.path )
            return

DELETE simply opens the target file with the w attribute and closes it without doing anything to make it an empty file.

rest_server

rest_server


def rest_server(port):
    httpd_rest = http.server.ThreadingHTTPServer(("", port), RestHandler)
    httpd_rest.serve_forever()

It is a function that simply starts the server on the port specified by the argument.

main

main


def main():
    #Server startup
    port_rest  = 3333
    try:
        t1 = threading.Thread(target=rest_server,  args=(port_rest,),  daemon = True)

        t1.start()

        while True: time.sleep(1)

    except (KeyboardInterrupt, SystemExit):
        print("exit")
        sys.exit()

I am trying to start the server in a separate thread. In addition, the program ends when an end interrupt (Ctrl + C) is received from the keyboard.

Operation check

When you start the script, the server will start on port 3333, so try executing the following command.

curl -X POST -H 'Content-Type:application/json' -d '{"key":"val"}' localhost:3333/dat
curl -X GET localhost:3333/dat
curl -X DELETE localhost:3333/dat

The command is localhost because it is from the same machine as the server. If you use another machine, try using the IP address of the machine that will be the server.

the end

I was able to build a REST server for the time being. If you want to add an API, you can nest path analysis more and more. Also, since you can easily call another command, you can extend it so that it can also be used for system management.

Please try it.

Recommended Posts

Create a (simple) REST server
Create a simple textlint server
Create a pseudo REST API server using GitHub Pages
How to create a simple TCP server / client script
Create a Unix Domain Socket server
Write a super simple TCP server
Create a simple GUI app in Python
Build a simple WebDAV server on Linux
Create a simple web app with flask
Set up a simple HTTPS server in Python 3
Set up a simple HTTPS server with asyncio
Let's create a REST API using SpringBoot + MongoDB
Start a simple Python web server with Docker
Create a simple momentum investment model in Python
Set up a simple SMTP server in Python
How to create a Rest Api in Django
Create a "Hello World" (HTTP) server with Tornado
Create a web server in Go language (net/http) (2)
Create a Python module
Create a Bootable LV
Create a Python environment
Create a slack bot
Create a Todo app with Django REST Framework + Angular
Set up a simple local server on your Mac
A simple Python HTTP server that supports Range Requests
Create a Todo app with the Django REST framework
Create a home music server with Centos8 + Universal Media Server
Create a web server in Go language (net / http) (1)
Create a fake Minecraft server in Python with Quarry
[Vagrant] Set up a simple API server with python
Create a Wox plugin (Python)
Create a local pypi repository
Create a function in Python
Create a dictionary in Python
A simple sample of pivot_table.
Django beginners create simple apps 3
Django beginners create simple apps 1
Launch a simple WEB server that can check the header
Create a homepage with django
How to specify a public directory Python simple HTTP server
[Python Kivy] How to create a simple pop up window
Create a python numpy array
Create a dummy data file
Django beginners create simple apps 2
Create a simple video analysis tool with python wxpython + openCV
Create a Django login screen
Create a simple Python development environment with VSCode & Docker Desktop
Create a heatmap with pyqtgraph
Create a classroom on Jupyterhub
Simple HTTP Server for python
Create a directory with python
Create a simple CRUD app using Django's generic class view
Django beginners create simple apps 5
Create a rudimentary ELF packer
I tried to create a server environment that runs on Windows 10
[C language] [Linux] Try to create a simple Linux command * Just add! !!
Overview of how to create a server socket and how to establish a client socket
I tried to create a simple credit score by logistic regression.
Rails users try to create a simple blog engine with Django
Create a simple scheduled batch using Docker's Python Image and parse-crontab
Create a simple Python development environment with VS Code and Docker