loading...

A python journey. Episode 3

ochaye3000 profile image ed ・5 min read

Right. Episode 3, huh! What have I been up to? Well, in true fast show style this week I have been mostly using flask.

So what's flask? It's a web framework, it's like express if you're coming from node, but it's also not like express. You can quickly create a little web API and run it by creating some simple code such as:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def run_my_endpoint():
    return 'Some people might be tempted to write hello world here'

if __name__ == "__main__":
    app.run(port=5000, debug=True)

So, we've all seen a million Hello world's in the ridiculous amounts of hours we've spent in front of computer screens before. Every language has them so why should you read on? What are we learning here? Dunders! yes any non-Pythonistas out there, dunders! and to help us (d)understand what that is it's basically special powers and methods.

Why the hell is my app.py file called main by python? Well, because if it's the parent file running (which it is in this case) then let me explain that it's not insane, nor too much pain so just retain that the __name__ will be __main__ (I really, really wanted to be able to fit in something about the rain falling mainly on the plane here)

Back to flask... the lowdown here, much like most other api/web libs is the package will match to URLs and run the related code. As you see in that astonishingly terse example above if you run this program and open a browser and navigate to localhost:5000/ you'll see the magic.

Yeah, that's great, so now what? well, now you're ready to learn about endpoint design and URL design, about restful principles and stuff that's not specific to python and therefore out of the scope of this article.

So the article is more about how I've been learning it. I've been mostly writing shitty code. Do I know how to write a rest API in python? Absolutely not! I'm resigned to writing absolute bullshit code that will make me squirm tomorrow, next week, next month and so on. Being openly rubbish is only uncomfortable for a brief spell initially.

from flask import Flask, jsonify, request

app = Flask(__name__)

stores = [
    {
        "name": "Example Store",
        "items": [
            {"name": "item one", "price": 21.99},
            {"name": "item two", "price": 17.99},
        ],
    },
    {
        "name": "Another Store",
        "items": [
            {"name": "item three", "price": 55.50},
            {"name": "item four", "price": 109.99},
        ],
    },
]


@app.route("/")
def home():
    return "hello, world"


@app.route("/store", methods=["POST"])
def create_store():
    request_data = request.get_json()
    new_store = {"name": request_data["name"], "items": []}
    stores.append(new_store)
    return jsonify(new_store)


@app.route("/store/<string:name>")
def get_store(name):
    for store in stores:
        if store["name"] == name:
            return jsonify(store["items"])
    return jsonify({"message": "No matching store found"})


@app.route("/store")
def get_stores():
    return jsonify({"stores": stores})


@app.route("/store/<string:name>/item", methods=["POST"])
def create_item_in_store(name):
    request_data = request.get_json()
    for store in stores:
        if store["name"] == name:
            new_item = {"name": request_data["name"], "price": request_data["price"]}
            store["items"].append(new_item)
            return jsonify(new_item)
    return jsonify({"message": "No matching store found"})


@app.route("/store/<string:name>/item")
def get_item_in_store(name):
    for store in stores:
        if store["name"] == name:
            return jsonify({"items": store["items"]})
    return jsonify({"message": "No matching store found"})


app.run(port=5000)

Step one, in-memory data stores as hardcoded data. And there we go, readers - this is absolute junk-town stuff. The point I'm trying to make here is I've stripped as much noise away as possible to get to one or two digestible takeaways. I've aimed for the lowest cognitive load. So here I learned:

  • Methods which are programmer-declared must be passed in as a list.
  • The default method is GET. If not specified otherwise it's a GET.

OK standard, see that everywhere. Box ticked. Nothing too special about basic requests.

Step two, I picked up some endpoint syntax declaration. My bat sense also started to tingle that I'm overthinking most of it.

from flask import Flask, request, jsonify
from flask_restful import Resource, Api, reqparse
from security import authenticate, identity
from flask_jwt_extended import (
    JWTManager,
    jwt_required,
    create_access_token,
    get_jwt_identity,
)

app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = "flaskymcflaskface"
api = Api(app)
jwt = JWTManager(app)


@app.route("/auth", methods=["POST"])
def login():
    if not request.is_json:
        return jsonify({"msg": "Missing JSON in request"}), 400

    username = request.json.get("username", None)
    password = request.json.get("password", None)
    if not username:
        return jsonify({"msg": "Missing username parameter"}), 400
    if not password:
        return jsonify({"msg": "Missing password parameter"}), 400

    if authenticate(username, password):
        # Identity can be any data that is json serializable
        access_token = create_access_token(identity=username)
        return jsonify(access_token=access_token), 200


items = []


class ItemList(Resource):
    def get(self):
        if len(items) > 0:
            return {"items": items}
        return {"items": None}, 404


class Item(Resource):
    parser = reqparse.RequestParser()
    parser.add_argument(
        "price",
        type=float,
        required=True,
        help="The price field cannot be blank, or missing",
    )

    @jwt_required
    def get(self, name):
        item = next(filter(lambda x: x["name"] == name, items), None)
        return {"item": item}, 200 if item else 404

    def post(self, name):
        # checks existing
        if next(filter(lambda x: x["name"] == name, items), None) is not None:
            return (
                {"message": "An item with name '{}' already exists".format(name)},
                400,
            )
        # doesn't exist so posts a new item
        data = Item.parser.parse_args()
        item = {"name": name, "price": data["price"]}
        items.append(item)
        return item, 201

    def delete(self, name):
        global items
        items = list(filter(lambda x: x["name"] != name, items))
        return {"message": "Item deleted"}

    def put(self, name):
        data = Item.parser.parse_args()
        item = next(filter(lambda x: x["name"] == name, items), None)
        if item is None:
            item = {"name": name, "price": data["price"]}
            items.append(item)
        else:
            item.update(data)
        return item


api.add_resource(Item, "/item/<string:name>")
api.add_resource(ItemList, "/items")

app.run(port=5000, debug=True)

Step three we're on to version two of this tiny API, I added some more junk by hacking-in an inelegant flask-jwt-extended implementation so I could pick up some JWT and auth recipes in python. This is a work in progress and I'll need to go sniffing around some decent samples on GitHub to look for patterns and pitfalls of others before I have something that resembles decent code. For the moment it fires back a JWT and yes you can attach it to a request and it'll get past a @jwt_required decorated call. It works.

Step four I picked up some tidbits on using/extending flask_restful.Resource. I like this, you create a class and extend Resource, you implement your restful verbs as required. It's a nice pattern.

Summary
I'm still refining this example and I've moved on to SQLAlchemy now and added some additional resources and added ForeignKeys etc. More about that on another post. The learning journey is a funny thing, I've gone from verbose, enterprise problems to small trivial code challenges and the ease or one-liner potential of python is still what screws me over when I think through a problem. I do not yet think like python, or think in python. If you made it this far, thanks for your time. Best wishes.

Posted on by:

ochaye3000 profile

ed

@ochaye3000

Reformed javascripter who plays with python.

Discussion

pic
Editor guide