פיתוח שרת API עם Python ו Flask

12/06/2018

פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.

פלאסק היא סביבה לפיתוח Web בשפת Python. זוהי Micro Framework המבוססת על פלאגינים ולכן ליבת הספריה מאוד רזה, מה שאומר שקל מאוד ללמוד איך להתחיל להשתמש בה. באותה נשימה אגיד גם שקל לעשות טעויות. החברה הגדולה ביותר שמצאתי שמשתמשת בפלאסק היא Pinterest שם פלאסק היא הבסיס לשרת ה API של החברה, שרת המטפל במיליארדי בקשות ביום.

בפוסט זה אתאר בקצרה את פלאסק דרך כתיבת מספר תוכניות ונראה מה צריך כדי לבנות Backend API איתו.

1. התקנה ותוכנית ראשונה

כדי להתקין את פלאסק מספיק להתקין את החבילה. משורת הפקודה תוכלו לרשום:

$ python -m pip install flask

או בכל דרך אחרת בה אתם מתקינים חבילות, למשל מהתפריטים של PyCharm.

תוכנית ראשונה בפלאסק יכולה להיראות כך:

from flask import Flask

app = Flask('helloworld')

# Decorator defines a route
# http://localhost:5000/
@app.route('/')
def index():
    return "Hello World!"

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

הקוד מאוד מזכיר את סינטרה או express אם עבדתם בסביבות אלה. התוכנית מגדירה נתיב יחיד בשם / וכל מי שיפנה לשם יקבל בתשובה את המחרוזת Hello World. נשמור את התוכנית בקובץ בשם helloworld.py ונפעיל עם:

$ python helloworld.py

כעת אפשר לגלוש לשרת המקומי לפורט 5000 כדי לקבל על המסך את הטקסט Hello World.

2. החזרת JSON Object

בשביל לבנות API יעיל נרצה להחזיר למשתמש אוביקט JSON במקום טקסט פשוט. הפונקציה jsonify של פלאסק תעזור במשימה זו. הקוד הבא כבר מחזיר אוביקט JSON במקום טקסט פשוט:

from flask import Flask, jsonify

app = Flask('helloworld')

# Decorator defines a route
# http://localhost:5000/
@app.route('/')
def index():
    return jsonify({ 'text': 'Hello World!' })

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

הפונקציה jsonify כבר תדאג להגדיר עבורכם את ה content-type לערך application/json.

3. הגדרת CORS

החלק הבא שחשוב ביצירת API הוא הגדרת כותרות HTTP עבור לקוחות שמגיעים מדומיין חיצוני. מאחר ואין לנו דפי HTML אלא רק API Endpoints כותרות אלה הן שמאפשרות ללקוחות לעבוד עם ה API שנבנה.

אפשר להגדיר את הכותרות ידנית אבל הרבה יותר נוח להתקין חבילת הרחבה בשם flask-cors. זה גם ייתן לנו הזדמנות לראות איך להשתמש בחבילת הרחבה. התקינו את החבילה flask-cors דרך שורת הפקודה או בכל דרך אחרת. לאחר מכן עדכנו את קוד התוכנית לתוכן הבא:

from flask import Flask, jsonify
from flask_cors import CORS

app = Flask('helloworld')
CORS(app)

# Decorator defines a route
# http://localhost:5000/
@app.route('/')
def index():
    return jsonify({ 'text': 'Hello World!' })

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

4. שמירת מידע בבסיס הנתונים

האתגר הבא יהיה להוסיף לתוכנית את SQL Alchemy ואיתו את flask-sqlalchemy. ספריות אלו יאפשרו לנו גישה קלה לבסיס הנתונים באמצעות ORM. נתקין תחילה את הספריה:

$ python -m pip install flask-sqlalchemy

כעת ניצור מחלקה המייצגת טבלא בבסיס הנתונים:

class Message(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.Text, nullable=False)

ב API שנרצה ליצור יהיו הודעות וכל אחד יוכל להוסיף הודעה. להודעה יהיה רק טקסט כי אנחנו בונים חדר שיחה אנונימי (או מתעצלים לכתוב גם טבלאות למשתמשים, תבחרו את ההסבר שמדבר אליכם יותר).

בכל מקרה ב SQL Alchemy עמודות הטבלא מיוצגות כשדות מידע סטטיים במחלקה ולכן ההגדרות id ו text רשומות שם עם כל מאפייניהן.

כדי לאתחל את בסיס הנתונים עדכנתי את התוכנית כך שתיצור אוביקט db עם חיבור לבסיס הנתונים ותקבל פרמטר משורת הפקודה לפיו היא תחליט אם להפעיל שרת או לאתחל את בסיס הנתונים. הקוד המלא נראה כעת כך:

from flask import Flask, jsonify
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
import sys
from pathlib import Path

app = Flask('helloworld')

app_dir = Path(sys.argv[0]).resolve().parent
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{app_dir}/test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
CORS(app)

class Message(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.Text, nullable=False)

# Decorator defines a route
# http://localhost:5000/
@app.route('/')
def index():
    return jsonify({ 'text': 'Hello World!' })

if __name__ == '__main__':
    if len(sys.argv) == 1:
        sys.exit(f'Usage: {sys.argv[0]} <start|init>')

    if sys.argv[1] == 'init':
        db.create_all()
        sys.exit()

    if sys.argv[1] == 'start':
        app.run()
        sys.exit()

בראש התוכנית תמצאו את הגדרות בסיס הנתונים ובסופה את הקוד שמאתחל את בסיס הנתונים אם מפעילים את הקובץ עם הפרמטר init. בסיס הנתונים לדוגמא שלנו הוא מסוג sqlite ונשמר כקובץ בתיקיית היישום.

הפעילו פעם אחת את התוכנית עם הפרמטר init כדי לראות שקיבלתם את בסיס הנתונים הראשוני ואז נהיה מוכנים לקבל פרמטרים וליצור בו רשומות בהתאמה:

$ python helloworld.py init

5. פרמטרים ושמירת מידע

נרצה להוסיף לתוכנית עוד שתי API Endpoints: האחת תקבל בקשות GET ותחזיר את כל ההודעות והשניה תקבל בקשות POST ותוסיף הודעה.

בשביל להחזיר את רשימת ההודעות עלינו להמיר כל הודעה ל Dictionary כדי שנוכל לקודד אותה כ JSON. השינוי הבא במחלקה Message יעזור לנו להתקדם:

class Message(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.Text, nullable=False)

    def as_dict(self):
        return {'text': self.text}

איתו נוכל לכתוב את הפונקציה:

def messages_as_json():
    return jsonify([m.as_dict() for m in Message.query.all()])

ואז להמשיך לכתיבת ה API Endpoint:

@app.route('/messages', methods=['GET'])
def index():
    return messages_as_json()

או בעברית: בכל פעם שמגיעה בקשת GET לנתיב /messages יש לקרוא את כל ההודעות השמורות בבסיס הנתונים ולהחזיר את כל הרשימה כ JSON. בעולם האמיתי נרצה גם להגביל את מספר ההודעות המוחזרות וליישם אפשרות למעבר בין דפים.

עבור בקשות POST נרצה לקרוא את הטקסט מהבקשה ולהוסיף אוביקט חדש לבסיס הנתונים. זה בדיוק מה שעושה הקוד הבא:

@app.route('/messages', methods=['POST'])
def create():
    text = request.values['text']
    msg = Message(text=text)
    db.session.add(msg)
    db.session.commit()
    return messages_as_json()

6. סיכום: ויש לנו API

התוכנית שכתבנו יודעת להתחבר לבסיס הנתונים ולענות על שני סוגים של בקשות: שליפת מידע או שמירת מידע חדש. התאמה של המידע לבסיס נתונים יותר מתוחכם תדרוש בסך הכל שינוי של המחלקה Message או הוספת מחלקות חדשות הדומות לה וממופות לבסיס הנתונים.

כמובן שבעולם האמיתי נרצה לחלק את התוכנית הזו לקבצים ותיקיות, נצטרך לעבור לבסיס נתונים מתוחכם יותר וכמובן לשרת ייצור במקום שרת הפיתוח של פלאסק. ועדיין, אני חושב שזה לא רע בשביל פחות מ-50 שורות קוד. התוכנית המלאה:

from flask import Flask, jsonify, request
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
import sys
from pathlib import Path

app = Flask('helloworld')

app_dir = Path(sys.argv[0]).resolve().parent
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{app_dir}/test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
CORS(app)

class Message(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.Text, nullable=False)

    def as_dict(self):
        return {'text': self.text}

@app.route('/messages', methods=['GET'])
def index():
    return messages_as_json()

@app.route('/messages', methods=['POST'])
def create():
    text = request.values['text']
    msg = Message(text=text)
    db.session.add(msg)
    db.session.commit()
    return messages_as_json()

def messages_as_json():
    return jsonify([m.as_dict() for m in Message.query.all()])

if __name__ == '__main__':
    if len(sys.argv) == 1:
        sys.exit(f'Usage: {sys.argv[0]} <start|init>')

    if sys.argv[1] == 'init':
        db.create_all()
        sys.exit()

    if sys.argv[1] == 'start':
        app.run()
        sys.exit()

נ.ב. בוובינר שהתקיים אתמול הצגתי צד אחד של פלאסק שכלל פיתוח אתר ושימוש ב Templates. אם בא לכם ללמוד גם על הצד ההוא מוזמנים לצפות בהקלטה (למנויים בלבד, כמו כל הוובינרים כאן. ואם אין לכם עדיין מנוי היום הוא יום מצוין בשביל לפתוח אחד).