4

I have code duplication in my API design for the object methods vs. the URL routing functions:

# door_model.py

class Door:                               
    def open(self):                       # "Door.open" written once...
       ...
# http_api.py (the HTTP server is separated from the real-world object models)

@app.route('/api/door/open')              # ... written twice
def dooropen():                           # ... written three times
    d.open()                              # ... written four times!

d = Door()

How to avoid this unnecessary duplication of names in a similar API design? (while keeping a separation between real-world object models vs. HTTP server).

Is there a general pattern to avoid unnecessary duplication of names when using an object model (with methods), and URL routes functions? (nearly a Model View Controller pattern)

See also Associate methods of an object to API URL routes with Python Flask.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Basj
  • 41,386
  • 99
  • 383
  • 673

3 Answers3

3

If we declare a route for every model action and do the same things for each (in your case, call the corresponding method with or without parameter), it will duplicate the code. Commonly, people use design patterns (primarily for big projects) and algorithms to avoid code duplications. And I want to show a simple example that defines one generic route and handles all requests in one handler function.

Suppose we have the following file structure.

application/
├─ models/
│  ├─ door.py
│  ├─ window.py
├─ main.py

The prototype of the Door looks like

# door.py

class Door:

    def open(self):
        try:
            # open the door
            return 0
        except:
            return 1

    def close(self):
        try:
            # close the door
            return 0
        except:
            return 1

    def openlater(self, waitseconds=2):
        print("Waiting for ", waitseconds)
        try:
            # wait and open the door
            return 0
        except:
            return 1

Where I conditionally set exit codes of the C, 0 for success and 1 for error or failure.

We must separate and group the model actions into one as they have a common structure.

+----------+----------+------------+----------------------+
| API base |  model   | action     | arguments (optional) |
+----------+----------+------------+----------------------+
| /api     | /door    | /open      |                      |
| /api     | /door    | /close     |                      |
| /api     | /door    | /openlater | ?waitseconds=10      |
| /api     | /window  | /open      |                      |
| /api     | /<model> | /<action>  |                      |
+----------+----------+------------+----------------------+

After we separate our groups by usage interface, we can implement a generic handler for each.

Generic handler implementation

# main.py

from flask import Flask, Response, request
import json
from models.door import Door
from models.window import Window

app = Flask(__name__)

door = Door()
window = Window()

MODELS = {
    "door": door,
    "window": window,
}

@app.route("/api/<model>/<action>")
def handler(model, action):
    model_ = MODELS.get(model)
    action_ = getattr(model_, action, None)
    if callable(action_):
        try:
            error = action_(**request.args)
            if not error:
                return Response(json.dumps({
                    "message": "Operation succeeded"
                }), status=200, mimetype="application/json")
            return Response(json.dumps({
                "message": "Operation failed"
            }), status=400, mimetype="application/json")
        except (TypeError, Exception):
            return Response(json.dumps({
                "message": "Invalid parameters"
            }), status=400, mimetype="application/json")
    return Response(json.dumps({
        "message": "Wrong action"
    }), status=404, mimetype="application/json")

if __name__ == "__main__":
    app.run()

So you can control the actions of the models by using different API paths and query parameters.

Basj
  • 41,386
  • 99
  • 383
  • 673
Artyom Vancyan
  • 5,029
  • 3
  • 12
  • 34
  • Thank you for this proposal, but the problem is that then the hardware "model" (`class Door`) now mixes hardware interaction (close, open the doors, etc.) but also HTTP API, requests, etc. which are fundamentally different things. I would prefer a separation of concerns (`class Door` contains only code relative to the hardware interaction, and `http_api.py` contains only code relative to HTTP). – Basj Jun 02 '22 at 11:29
  • Well, do you want to avoid code duplication or design the application with separated classes and components? The `main.py` of my example corresponds to the working behavior of `http_api.py`. I called it `main.py` to indicate the standpoint of the app. – Artyom Vancyan Jun 02 '22 at 11:48
  • Both in fact @ArtyomVancyan :) A model for a hardware component (example: a hardware Door eletronic controller) should probably contain only hardware-related code, and nothing specific to the HTTP server we use. That's why I separated them in my original code example. But I agree it's difficult to 1) keep this important separation 2) avoid code duplication as much as possible. Thus this difficult question indeed! – Basj Jun 02 '22 at 11:54
  • Oh, I see. In any case, the `open`, `close`, or another method should return some value to inform the result status (has the door been opened or closed). That time you could handle the response data by returned status. – Artyom Vancyan Jun 02 '22 at 11:59
  • 1
    Nice solution, I marked as accepted, and bounty. Thanks! – Basj Jun 02 '22 at 12:48
2

You can create dynamic routes. A dynamic route for your case would be api/door/<action>.

Create a route like this to have a dynamic url:

@app.route('api/door/<action:str>')
def door(action):
    
   door = Door()
   if action in door.actions:
       if action.lower() == 'open':
           door.open()
           r = 'oppened door'

       return r
        
   

Create a class variable called actions to make the code work. For example like this actions = ['open','close']

NoNameAv
  • 423
  • 2
  • 14
2

You can leverage the flask blueprint pattern

In your http_api.py use

app = Flask(__name__)
# ... configs to app instance ...
app.register_blueprint(door_routes, url_prefix="/api/door")

In Your api/door.py

door_routes = Blueprint("door_routes", __name__)

door = Door()
@door_routes.route("/open")
def open():
    d.open() 
    return 

#other routes

Or alternatively, you can use:

class Door():
     def open(self):
        print("opened")

d = Door()

@app.route("/api/door/<action>", methods=["POST", "GET"])
def catch_all(action):
    try:
       function = getattr(d,action)
       resp = function()
       #return jsonify(resp),200 or the below line
       return f"Executed {action}"
    except Exception as e:
       return f"URL {action} not found" 

    
    
ShivaGaire
  • 2,283
  • 1
  • 20
  • 31