0

This has been vexing me for a while now. I am trying to create a very simple REST-like interface (without using third-party libraries, which I know are available).

The idea behind it is that I can have a directory, for example mylib, where I can drop in python files, like do_something.py and, by POSTing to http://localhost/do_something the code will spring into life and do something!

I think I have managed to get somewhere near to my goal with the following structure: Project folder contains example.py and a folder called mylib, which contains a file called init.py and my_module.py

The contents of the files are as follows.

example.py
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
import json, logging
from mylib import my_module

class MyRequestHandler (BaseHTTPRequestHandler):

    # Send JSON responses
    # -----------
    def send_json(self, json_message, response_code=200):
        self.send_response(response_code)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        self.request.sendall(json.dumps(json_message).encode())


    # Get JSON requests
    # ----------
    def get_json(self):
        body = self.rfile.read(int(self.headers.get('Content-Length')))
        if (body):
            try:
                receivedData = json.loads(body.decode())
            except:
                self.send_json({"Status": "Error", "Message": "Invalid JSON received"}, 400)
                receivedData = None
        else:
            receivedData = None
        return receivedData


    # POST
    # ---------
    def do_POST(self):

        module_to_call = (self.path).replace('/', '.')[1:]
        if module_to_call.endswith('.'): # Remove trailing dot
            module_to_call = module_to_call[:-1]
        print("Path is: '" + module_to_call + "'")

        # invoke function
        module_to_call = getattr(self, module_to_call)
        response = module_to_call()
        self.send_json(response)

    # GET
    # --------
    def do_GET(self):

        pass


# -----------------------------------------------------------------------------
# Server startup code
# -------------------
def start_server():


# Begin serving
# -------------
    port = 8003
    server = HTTPServer(('', port), MyRequestHandler)
    print(("Server now running on port {0} ...").format(port))

    server.serve_forever()


# -----------------------------------------------------------------------------
# Start the Server
# ----------------
if __name__ == '__main__':
    start_server()

my_module.py

def my_module():
    print("Hello World!")
    return{'Greeting': 'Hello World!'}

When I fire up the server and attempt to POST to http://localhost:8003/my_module, I get the following output:

Server now running on port 8003 ...
Path is: 'my_module'
----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 59541)
Traceback (most recent call last):
  File "C:\Users\Test\AppData\Local\Programs\Python\Python35-32\lib\socketserver.py", line 313, in _handle_request_noblock
    self.process_request(request, client_address)
  File "C:\Users\Test\AppData\Local\Programs\Python\Python35-32\lib\socketserver.py", line 341, in process_request
    self.finish_request(request, client_address)
  File "C:\Users\Test\AppData\Local\Programs\Python\Python35-32\lib\socketserver.py", line 354, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "C:\Users\Test\AppData\Local\Programs\Python\Python35-32\lib\socketserver.py", line 681, in __init__
    self.handle()
  File "C:\Users\Test\AppData\Local\Programs\Python\Python35-32\lib\http\server.py", line 422, in handle
    self.handle_one_request()
  File "C:\Users\Test\AppData\Local\Programs\Python\Python35-32\lib\http\server.py", line 410, in handle_one_request
    method()
  File ".\example.py", line 43, in do_POST
    module_to_call = getattr(self, module_to_call)
AttributeError: 'MyRequestHandler' object has no attribute 'my_module'
----------------------------------------

This makes perfect sense, since 'MyRequestHandler" does not have an attribute "my_module"! What I can't wrap my head around, is how to fix this?

Should I pass "mylib" into MyRequestHandler? Should I perform the import within the class (but then the functionality would only be available within the class)?

I'm trying to keep things clean and simple, so that even a Python novice (like I seem to be!) can just write a standalone script, drop it into "mylib" and everything "just works". The novice could visit the web address of their script and have it magically run.

Any help or advice would be gratefully received.

PeterByte
  • 3,534
  • 2
  • 20
  • 16
  • 1
    Since you import the module into the global scope of your script you can access it from inside any function with `globals()[module_to_call]`. However, this is not what you really want because you manually import `my_module`, which won't "just work" if somebody drops in a new file. – MB-F Feb 09 '17 at 12:09
  • I dont know what's the use-case of the thing you're trying to do, but I'd recommend you to not reinventing a wheel but to use a lightweight framework instead (eg. Flask), because that's what you'll end with in the end anyway. – yedpodtrzitko Feb 09 '17 at 12:09
  • 2
    Possible duplicate of [Dynamic module import in Python](http://stackoverflow.com/questions/301134/dynamic-module-import-in-python) – MB-F Feb 09 '17 at 12:13
  • 1
    It is not an obvious duplicate, but in the end dynamic import will be the solution to *drop it into "mylib" and everything "just works"*. – MB-F Feb 09 '17 at 12:15
  • Thank you so much @kazemakase! Now that you have mentioned globals() it seems so obvious. And you are right that dynamic module importing was not the most obvious answer to me (even though it was staring me in the face, having read-up on it last year), but it is the answer I sought! I don't know how to give you any credit for your answers as they are comments, rather than answers... I've upvoted them anyway. – PeterByte Feb 09 '17 at 13:23
  • Knowing it helped is credit enough :) – MB-F Feb 09 '17 at 13:32

1 Answers1

2

Use the __import__() method:

temp = __import__('mylib', globals(), locals(), ['module_to_call'], -1)
response = temp.module_to_call()

I use 2.6 at work, and this is usually used by those using even 2.7 because the importlib module is far more robust in 3. If you are using 3 you can do the following:

from importlib import import_module

temp = import_module('mylib')

but now you have to use getattr to get the function you want to be called

func_to_call = getattr(temp, 'module_to_call')
response = func()

Or you can have a dictionary of functions in another module, but that will require a lot of work as that dictionary grows.

  • Whilst I like this answer, I am also a little wary of it as I get the impression that the use of `__import__` is a bit contentious. e.g. http://stackoverflow.com/a/26476048/1798547 – PeterByte Feb 09 '17 at 14:46
  • 1
    @LesterBurnham added a bit to the answer, so you can see other ways. This is common in a lot of libraries. – Amaury Larancuent Feb 09 '17 at 15:37