مقدمه

برای شروع :

sudo pip install flask

فرض ما بر این است که شما با مبانی REST آشنا هستید.

Flask یک میکروفریمورک پایتون بر مبنای Werkzeug (یک کتابخانه ابزار WSGI) می باشد.

Flask انتخاب خوبی برای یک REST API می باشد زیرا :

  • به زبان پایتون نوشته شده است
  • استفاده راحتی دارد
  • انعطاف پذیر است
  • چندین راهکار مختلف برای پیاده سازی دارد
  • دارای تجزیه request مبتنی بر REST است

من معمولا از curl برای تست تقاضاها روی سرور استفاده میکنم. انتخاب خوب دیگر در این زمینه REST Console برای Google Chrome است.

به عنوان قرارداد در این مقاله، هرگاه یک پاسخ از سرور ارایه شد، قبلا یک تقاضا با پارامترهای مد نظر برای تولید آن پاسخ ارایه شده است. خود تقاضا قسمتی از پاسخ نمی باشد.

منابع (RESOURCES)

اجازه دهید با نوشتن یک برنامه کامل که به تقاضاها در ریشه، /articles و /articles/:id پاسخ میدهد.

from flask import Flask, url_for
app = Flask(__name__)

@app.route('/')
def api_root():
    return 'Welcome'

@app.route('/articles')
def api_articles():
    return 'List of ' + url_for('api_articles')

@app.route('/articles/<articleid>')
def api_article(articleid):
    return 'You are reading ' + articleid

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

میتوانید از curl برای تقاضا استفاده کنید:

curl http://127.0.0.1:5000/

پاسخ متناظر عبارت خواهد بود از :

GET /
Welcome

GET /articles
List of /articles

GET /articles/123
You are reading 123

هدایتگرها (Routes) میتوانند از تبدیلگرهای متفاوتی در تعریفشان استفاده کنند

@app.route('/articles/<articleid>')

و میتوانند با موارد زیر جایگزین شوند:

@app.route('/articles/<int:articleid>')
@app.route('/articles/<float:articleid>')
@app.route('/articles/<path:articleid>')

پیش فرض string است که هر متنی که اسلش نداشته باشد قبول میکند.

تقاضاها

میتوانید به قسمت Incoming Request Data در مستندات Flask نگاهی بیاندازید.

پارامترهای GET

با یک برنامه کامل که به تقاضاها در /hello پاسخ میدهد و یک پارامتر اختیاری GET را مدیریت میکند شروع میکنیم:

from flask import request

@app.route('/hello')
def api_hello():
    if 'name' in request.args:
        return 'Hello ' + request.args['name']
    else:
        return 'Hello John Doe'

سرور به صورت زیر پاسخ میدهد:

GET /hello
Hello John Doe

GET /hello?name=Luis
Hello Luis

متدهای تقاضا ( افعال HTTP )

کد قبلی را تغییر میدهیم تا افعال HTTP دیگری را هم مدیریت کند:

@app.route('/echo', methods = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
def api_echo():
    if request.method == 'GET':
        return "ECHO: GET\n"

    elif request.method == 'POST':
        return "ECHO: POST\n"

    elif request.method == 'PATCH':
        return "ECHO: PACTH\n"

    elif request.method == 'PUT':
        return "ECHO: PUT\n"

    elif request.method == 'DELETE':
        return "ECHO: DELETE"

در هنگام استفاده از curl از گزینه -X میتوان برای مشخص کردن نوع تقاضا استفاده کرد.

curl -X PATCH http://127.0.0.1:5000/echo

پاسخ به متدهای مختلف عبارت خواهند بود از :

GET /echo
ECHO: GET

POST /ECHO
ECHO: POST

و بقیه به هم به همین ترتیب.

هدرها و داده های تقاضا

معمولا POST و PATCH حاوی داده هستند که این داده های میتوانند در یکی از فرمتهای Plain text,JSON, XML و یا فرمت خاص خودتان یا یک فایل باینری باشند.

دسترسی به هدرهای HTTP از طریق دیکشنری request.headers (یک شی دیکشنری مانند) و دسترسی به داده های تقاضا از طریق رشته request.data انجام میشود. برای راحتی اگر mimetype از نوع application/json باشد rquest.json شامل JSON استخراج شده خواهد بود.



from flask import json

@app.route('/messages', methods = ['POST'])
def api_message():

    if request.headers['Content-Type'] == 'text/plain':
        return "Text Message: " + request.data

    elif request.headers['Content-Type'] == 'application/json':
        return "JSON Message: " + json.dumps(request.json)

    elif request.headers['Content-Type'] == 'application/octet-stream':
        f = open('./binary', 'wb')
        f.write(request.data)
                f.close()
        return "Binary message written!"

    else:
        return "415 Unsupported Media Type ;)"

برای مشخص کردن نوع محتوا به وسیله curl خواهیم داشت:

curl -H "Content-type: application/json" \
-X POST http://127.0.0.1:5000/messages -d '{"message":"Hello Data"}'

ارسال فایل با استفاده از curl:

curl -H "Content-type: application/octet-stream" \
-X POST http://127.0.0.1:5000/messages --data-binary @message.bin

پاسخ ها به درخواستهای با content-type متفاوت :

POST /messages {"message": "Hello Data"}
Content-type: application/json
JSON Message: {"message": "Hello Data"}

POST /message <message.bin>
Content-type: application/octet-stream
Binary message written!

بخاطر داشته باشید که Flask میتواند فایل های POST شده از فرمهای HTML را با استفاده از request.files هندل کند و curl میتواند آن را با پارامتر -F شبیه سازی کند.

پاسخ ها (Responses)

پاسخ ها با استفاده از کلاس Response در Flask مدیریت میشوند.

from flask import Response

@app.route('/hello', methods = ['GET'])
def api_hello():
    data = {
        'hello'  : 'world',
        'number' : 3
    }
    js = json.dumps(data)

    resp = Response(js, status=200, mimetype='application/json')
    resp.headers['Link'] = 'http://luisrei.com'

    return resp

برای نمایش هدرهای HTTP پاسخ با استفاده از curl از پارامتر -i استفاده کنید.

curl -i http://127.0.0.1:5000/hello

پاسخ برگردانده شده از سرور همراه با هدرها عبارت خواهد بود از:

GET /hello
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 31
Link: http://luisrei.com
Server: Werkzeug/0.8.2 Python/2.7.1
Date: Wed, 25 Apr 2012 16:40:27 GMT
{"hello": "world", "number": 3}

در واقع Mimetype همان content-type بدون اطلاعات اضافی است ( مانند charset, encoding, language, ...).

مثال قبلی میتواند با استفاده از یکی متدهای آسان ساز Flask برای تولید پاسخهای JSON جایگزین شود:

   from flask import jsonify

و همچنین

 resp = Response(js, status=200, mimetype='application/json')

با

resp = jsonify(data)
resp.status_code = 200

که دقیقا همان پاسخی را تولید میکند که کد قبلی تولید میکرد.

مشخص کردن نوع mime هنگامی که میخواهید از mime دلخواه خودتان مانند application/vnd.example.v

+json استفاده کنید مفید خواهد بود.

کدهای وضعیت و خطاها

همانطور که میدانید 200 کد وضعیت پیشفرض برای تقاضاهای GET می باشد. در هر دو مثال استفاده از آن فقط برای نشان دادن آن بود. موارد خاصی وجود دارند که جایگزینی موارد پیشفرض لازم است. یکی از این موارد مدیریت خطاهاست :

@app.errorhandler(404)
def not_found(error=None):
    message = {
            'status': 404,
            'message': 'Not Found: ' + request.url,
    }
    resp = jsonify(message)
    resp.status_code = 404

    return resp

@app.route('/users/<userid>', methods = ['GET'])
def api_users(userid):
    users = {'1':'john', '2':'steve', '3':'bill'}
    
    if userid in users:
        return jsonify({userid:users[userid]})
    else:
        return not_found()

پاسخ زیر را تولید میکند:

GET /users/2
HTTP/1.0 200 OK
{
    "2": "steve"
}

GET /users/4
HTTP/1.0 404 NOT FOUND
{
"status": 404, 
"message": "Not Found: http://127.0.0.1:5000/users/4"
}

پیغامهای خطای پیشفرض Flask میتوانند با دکریتور ‌@error_handler دوباره نگاری  شوند.

app.error_handler_spec[None][404] = not_found

حتی اگر API به پیامهای خطای اختصاصی نیاز نداشته باشد ولی از انواع mime مانند JSON , XML پشتیبانی کند، این ویژگی میتواند مهم باشد زیرا پیشفرض Flask برای خطاها HTML است.

یک snippet توسط Pavel Repin وجود دارد که نشان میدهد چگونه تمام پیامهای خطا با معادل JSON شان جایگزین گردند.

دسترسی ها (Authorization)

 snippet مفید دیگری که توسط Arimin Ronacher نوشته شده نشان میدهد که چگونه میتوان از اهراز هویت ساده HTTP اسفتاده کرد و این کد میتواد براحتی برای متدهای دیگر ویرایش شود. من هم کمی آن را تغییر داده ام :

from functools import wraps

def check_auth(username, password):
    return username == 'admin' and password == 'secret'

def authenticate():
    message = {'message': "Authenticate."}
    resp = jsonify(message)

    resp.status_code = 401
    resp.headers['WWW-Authenticate'] = 'Basic realm="Example"'

    return resp

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth: 
            return authenticate()

        elif not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)

    return decorated

و استفاده از این کد به راحتی جایگزینی تابع check_auth و استفاده از دکریتور requires_auth می باشد.

@app.route('/secrets')
@requires_auth
def api_hello():
    return "Shhh this is top secret spy stuff!"

پس حالا با یک تقاضای احراز هویت شده:

GET /secrets
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Example"
{
  "message": "Authenticate."
}

تقاضای تصدیق هویت شده با curl میتواند از طریق پارامتر -u برای استفاده از سیستم اهراز  هویت پایه HTTP و پارامتر -v برای نمایش هدرهای تقاضا استفاده شود.

curl -v -u "admin:secret" http://127.0.0.1:5000/secrets

که پاسخ مورد انتظار زیر را دربرخواهد داشت:

GET /secrets Authorization: Basic YWRtaW46c2VjcmV0
Shhh this is top secret spy stuff!

Flask از یک MultiDict باری ذخیره هدرها استفاده میکند. برای ارایه چندین مدل مختلف اهراز هویت به کاربران میتوانید براحتی خطوط WWW-Authenticate بیشتری را به هدر اضافه کنید.

resp.headers['WWW-Authenticate'] = 'Basic realm="Example"'
resp.headers.add('WWW-Authenticate', 'Bearer realm="Example"')

یا اینکه در یک خط از چندین مدل استفاده کنید.

خطازدایی و ثبت لاگ ساده

فعالسازی پیامهای  دیباگ HTML در حین توسعه برنامه میتواند براحتی با پاس دادن یک آرگومان انجام پذیرد

app.run(debug=True)

Flask از سیستم logging پایتون استفاده میکند، که کمی تنظیمات نیاز دارد:

import logging
file_handler = logging.FileHandler('app.log')
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)

@app.route('/hello', methods = ['GET'])
def api_hello():
    app.logger.info('informing')
    app.logger.warning('warning')
    app.logger.error('screaming bloody murder!')
    
    return "check your logs\n"
منبع: Implementing a RESTful Web API with Python & Flask