برنامه‌نویسی

ساخت API توسط Flask-RESTPlus، Flask و Swagger UI


۲۴ تیر ۱۳۹۹
ساخت API توسط Flask-RESTPlus، Flask و Swagger UI

هنگام کار بر روی پروژه‌های یادگیری ماشین، تصمیم گرفتم که یک برنامه کامل را توسعه دهم. این قضیه به توسعه APIها نیاز دارد که ما بتوانیم داده‌ها را وارد کنیم (post data) و از طرف دیگر پیشبینی‌ها را دریافت کنیم (get data). در اینجاست که Flask و Flask-RESTPlus وارد صحنه می‌شوند.

Flask این قابلیت را به توسعه‌دهنده می‌دهد که از توابع پایتونی برای ایجاد APIها استفاده کند. Flask-RESTPlus یک افزونه برای Flask است که قابلیت‌های بیشتر، نظیر ایجاد REST APIها را به Flask به می‌دهد. این افزونه نه تنها به ما اجازه می‌دهد که REST APIها را ایجاد کنیم، بلکه می‌توان تمام APIها را به Swagger UI اضافه کرد.

در این مقاله، نحوه توسعه یک برنامه توسط Flask به همراه چندین API و مقداری دیتا را بررسی خواهیم کرد. تمام کدهایی که در مقاله می‌بینید در این ریپازیتوری در دسترس هستند.

نصب

مراحل انجام این پروژه را با ایجاد یک محیط مجازی (virtual environment) برای کتابخانه‌های پایتون، با استفاده از دستور pipenv شروع می‌کنم. برای مشاهد تفاوت میان محیط‌های مجازی، می‌توانید به این مقاله مراجعه کنید. ابتدا Flask و Flask-RESTPlus را نصب می‌کنیم:

pipenv install flask
pipenv install flask-restplus

به هرحال اگر نخواهید از pipenv و یا محیط‌های مجازی استفاده کنید، می‌توانید به سادگی از pip استفاده کنید:

pip install flask
pip install flask-restplus

Import

پروژه را با واردکردن (import کردن) Flask از ماژول flask و Api و Resource از ماژول flask_restplus آغاز می‌کنم. از Api برای ساخت برنامه و از Resource به عنوان ورودی برای کلاس‌هایی که در داخل پروژه تعریف کردیم، استفاده می‌کنم:

from flask import Flask
from flask_restplus import Api, Resource

ساخت برنامه

برنامه flask را توسط تابع Flask، که نام آن را توسط __name__ تنظیم می‌کند، ایجاد کردم. در مرحله بعد از Api برای راه‌اندازی برنامه استفاده می‌کنم:

flask_app = Flask(__name__)
app = Api(app = flask_app)

name_space = app.namespace('main', description='Main APIs')

در اینجا namespace را ایجاد کردم. مفهوم و تصور کلی از آن بسیار ساده است. هرگاه APIهایی را در زیر یک namespace تعریف کنیم، آن‌ها در زیر یک دسته‌بندی در Swagger UI قرار می‌گیرند (Swagger UI را در ادامه بررسی می‌کنیم). در تعریف namespace، متغیر اول، مسیر را تعریف می‌کند و متغیر دوم توضیحی اضافه برای آن قسمت را تعریف می‌کند.

در مثال بالا آدرس namespace ایجاد شده به این صورت است: http://127.0.0.1:5000/main و توضیحات آن در Swagger برابر با Main APIs است.

تعریف APIها

در مرحله قبلی یک namespace ایجاد کردیم. از آنجایی که متغیری که namespace را در آن ایجاد کردیم، name_space نام دارد، برای ایجاد route در ادامه این namespace از @name_space.route("/") قبل ایجاد متدها استفاده می‌کنیم. در ادامه نیاز است که endpointها را در داخل این route ایجاد کنیم. متدهای داخل این route می‌تواند get() و یا post() و … باشد.

@name_space.route("/")
class MainClass(Resource):
	def get(self):
		return {
			"status": "Got new data"
		}
	def post(self):
		return {
			"status": "Posted new data"
		}

در این مثال، API از طریق مسیر http://127.0.0.1:5000/main در دسترس است. نام کلاس MainClass است که شامل دو متد get() و post() می‌شود. هرگاه درخواستی با متد GET به این API ارسال کنم، فیلدی بنام status با مقدار Got new data دریافت می‌کنم، زمان استفاده از متد POST، مقدار این فیلد برابر با Posted new data خواهد بود.

اجرای برنامه

حالا که همه چیز آماده است، باید برنامه را که در فایلی به نام basic.py ذخیره کرده‌ایم، توسط pipenv اجرا کنیم:

pipenv shell
FLASK_APP=basic.py flask run

اما اگر در مرحله اول از pip به جای pipenv استفاده کردید، از دستور زیر برای اجرای برنامه استفاده کنید:

FLASK_APP=basic.py flask run

Swagger UI

بهترین بخش Flask-RESTPlus این است که به صورت خودکار مستندات APIهایی که ایجاد کردیم ساخته می‌شود و در Swagger UI قابل مشاهده هستند. آدرس http://127.0.0.1:5000 را در مرورگر خود وارد کنید در نتیجه تمامی APIهایی که ایجاد کرده‌اید را مشاهده خواهید کرد.

رابط کاربری swagger

هردوی APIهایی که ایجاد کردیم، در زیر namespace با نام main که توضیحات آن Main APIs است، قابل مشاهده است. می‌توانیم هردو آن‌ها را به همراه کارکردشان توسط دکمه Try it out امتحان کنیم.

آزمایش API

از curl برای ایجاد یک درخواست GET و POST در ترمینال استفاده می‌کنم:

ایجاد درخواست به API

به هنگام استفاده از دستور curl، ابتدا از واژه curl به همراه نوع متد درخواست بعد از سوییچ -X استفاده کنید. در انتها هم مقصد را مشخص کنید. با توجه به پاسخی که از curl دریافت کردیم، می‌فهمیم که دیتای درستی را از هر دو API، یعنی هم GET و هم POST دریافت شده‌است.

استفاده‌های بیشتر

موارد قابل استفاده بیشتری در رابطه با Flask و FlaskRESTPlus وجود دارد. بیایید آن‌ها را عمیق‌تر بررسی کنیم تا بتوانیم آن‌ها را بهتر متوجه شویم. کدی که در ادامه بررسی خواهیم کرد، تحت عنوان app.py در ریپازیتوری موجود است.

می‌توانیم از متد POST برای ارسال دیتا و ذخیره‌شان و از متد GET هم برای دریافت و مشاهده آن‌ها بهره ببریم. بیایید در نظر بگیریم که پروژه‌ای داریم که در آن اسامی اشخاص را ذخیره و مدیریت می‌کنیم. یک endpoint با متد GET ایجاد کردیم که می‌توانیم توسط آن یک اسم را با استفاده id آن، دریافت کنیم، همچنین endpoint دیگری داریم که در آن از متد POST برای ذخیره هر اسم در مقابل یک id، استفاده می‌کنیم.

در اینجا، این مسیر را ایجاد کرده‌ام: http//127.0.0.1:5000/names/<int:id> که هر بار مقدار id را در آن ارسال می‌کنیم. برای ذخیره سازی آن‌ها، آبجکتی با نام list_of_names ایجاد کرده‌ام که برای دریافت و ارسال دیتا مورد استفاده قرار خواهد گرفت.

استفاده بیشتر از کتابخانه‌ها

تا به اینجای کار، Api ،Flask و Resource را وارد کدمان کرده‌ایم. در این بخش request را از ماژول flask وارد کدمان می‌کنیم، که به ما این امکان را می‌دهد یک درخواست را دریافت کنیم و بتوانیم از اطلاعات آن در قالبی مثل JSON استفاده کنیم. همچنین fields را از ماژول flask_restplus، برای تعریف انواع داده مختلف، مثل String وارد کد کردیم.

from flask import Flask, request
from flask_restplus import Api, Resource, fields

افزودن اطلاعات برنامه

همجنین می‌توانیم اطلاعات بیشتری به برنامه flaskمان اضافه کنیم. این اطلاعات در برخی موارد بسیار مفید خواهند بود و حتی در Swagger UI هم نمایش داده خواهند شد.

flask_app = Flask(__name__)
app = Api(app = flask_app, 
		  version = "1.0", 
		  title = "Name Recorder", 
		  description = "Manage names of various users of the application")

name_space = app.namespace('names', description='Manage names')

می‌توانیم title، version و description برنامه‌مان را تعریف کنیم. همچنین نام تنها namespace را names مقدار دادیم. حالا سرتیتر در Swagger UI باید مانند تصویر زیر باشد:

نمایش اطلاعات برنامه در Swagger UI

تعریف مدل‌ها

هرگاه بخواهیم اطلاعات را در قالب خاصی (JSON) ارسال و یا دریاف کنیم، از model استفاده می‌کنیم. ابتدا نام آن را مشخص می‌کنیم. در مرحله بعد اطلاعاتی که نیاز دارد، به همراه ویژگی‌های آن‌ها را تعریف می‌کنیم.

model = app.model('Name Model', 
		  {'name': fields.String(required = True, 
					 description="Name of the person", 
					 help="Name cannot be blank.")})

نام این مدل را Name Model تنظیم کردیم که شامل یک پارامتر با نام name می‌شود که این فیلد، یک فیلد ضروری (required) است و نمی‌تواند خالی باشد، همچنین توضیحات و متن راهنما برای آن تنظیم کردیم. API زمانی که قصد استفاده از این مدل را داشته باشد، انتظار دریافت دیتا در قالب JSON با کلیدی به نام name را دارد.

list_of_names = {}

برای ذخیره تمامی اسم‌ها، آن‌ها را در list_of_names ذخیره می‌کنم.

تعریف APIها

@name_space.route("/<int:id>")
class MainClass(Resource):

	@app.doc(responses={ 200: 'OK', 400: 'Invalid Argument', 500: 'Mapping Key Error' }, params={ 'id': 'Specify the Id associated with the person' })
	def get(self, id):
		try:
			name = list_of_names[id]
			return {
				"status": "Person retrieved",
				"name" : list_of_names[id]
			}
		except KeyError as e:
			name_space.abort(500, e.__doc__, status = "Could not retrieve information", statusCode = "500")
		except Exception as e:
			name_space.abort(400, e.__doc__, status = "Could not retrieve information", statusCode = "400")

	@app.doc(responses={ 200: 'OK', 400: 'Invalid Argument', 500: 'Mapping Key Error' }, params={ 'id': 'Specify the Id associated with the person' })
	@app.expect(model)		
	def post(self, id):
		try:
			list_of_names[id] = request.json['name']
			return {
				"status": "New person added",
				"name": list_of_names[id]
			}
		except KeyError as e:
			name_space.abort(500, e.__doc__, status = "Could not save information", statusCode = "500")
		except Exception as e:
			name_space.abort(400, e.__doc__, status = "Could not save information", statusCode = "400")

بیایید کد بالا را به بخش‌های کوچک‌تری تقسیم کنیم تا فهم آن آسان‌تر شود. ابتدا بخش POST را بررسی می‌کنیم، کارکرد بخش GET مشابه POST خواهد بود.

تعریف route و کلاس

@name_space.route("/<int:id>")
class MainClass(Resource):

از name_space که در بالاتر ایجاد کردیم، برای ایجاد route استفاده می‌کنیم. http://127.0.0.1:5000/main/<int:id> این آدرس انتظار دارد id، که از نوع عدد صحیح است را به عنوان پارامتر دریافت کند. نام کلاس MainClass است که Resource را به عنوان ورودی به آن داده‌ایم.

ایجاد مستندات برای API

@app.doc(responses={ 200: 'OK', 400: 'Invalid Argument', 500: 'Mapping Key Error' }, params={ 'id': 'Specify the Id associated with the person' })

استفاده از doc باعث می‌شود که بتوانیم مستنداتی برای APIمان در Swagger UI ایجاد کنیم. responses، انواع مختلفی از کدهای وضعیت مختلف HTTP را برای شرایط مختلف شامل می‌شود. برای هر کد وضعیت، توضیحاتی تعریف کرده‌ایم که اطلاعات بیشتری به کاربر می‌دهد. params، پارامتر‌هایی که انتظار دریافت آن‌ها را داریم، مشخص می‌کند. API در درخواستی که از سمت کاربر دریافت می‌کند انتظار وجود id را در URL دارد و همچنین یک متن راهنما برای نمایش به کاربر تعیین کرده‌ایم. تا به اینجای کار Swagger UI باید شبیه تصویر زیر باشد:

بخش POST در Swagger UI

پارامترها در بخش بالایی تصویر نمایش داده شده‌اند. تمام پاسخ‌هایی که ممکن است به سمت کاربر برگردد، به همراه توضیحات آن‌ها، در بخش پایینی تصویر قرار دارد.

تعریف متد

@app.expect(model)		
def post(self, id):
  try:
    list_of_names[id] = request.json['name']
    return {
      "status": "New person added",
      "name": list_of_names[id]
    }
  except KeyError as e:
    name_space.abort(500, e.__doc__, status = "Could not save information", statusCode = "500")
  except Exception as e:
    name_space.abort(400, e.__doc__, status = "Could not save information", statusCode = "400")

حالا می‌توانیم متد خودمان را تعریف کنیم. اما قبل از تعریف متد، @app.expect(model) را نوشته‌ایم که به این معناست API انتظار دریافت model را دارد. همچنین کدمان را در داخل بلوک try گذاشته‌ایم تا بتوانیم تمام خطاهای احتمالی را کنترل کنیم. request.json['name'] مقدار name دریافتی را برمی‌گرداند که درنتیجه می‌توانیم آن را ذخیره کنیم همانطور که می‌توانیم آن را در پاسخ به کاربر نیز ارسال کنیم. اگر کلید name وجود نداشته باشد، با خطای KeyError روبرو خواهیم شد و در پاسخ کاربر، کد وضعیت 500 را به همراه توضیحاتی که برای آن تعیین کردیم ارسال خواهیم کرد. در سایر خطاها کد وضعیت 400 را ارسال خواهیم کرد.

آزمایش برنامه

با دستور زیر برنامه را اجرا می‌کنیم:

FLASK_APP=app.py flask run

POST

ابتدا بعد از دریافت درخواست، مقدار name را از داخل آن بدست می‌آوریم و سپس آن را در مقابل یک id در list_of_names ذخیره می‌کنیم. همچنین وضعیت و نام شخص جدیدی که آن را ذخیره کردیم را نیز برمی‌گردانیم.

ارسال دیتا به API با مقدار id = 1 و name = Karan Bhanot
پاسخ درخواست قبل

خطا در درخواست POST

فرض کنیم در موردی فراموش شده که به name مقداری اختصاص بدهیم. در این حالت با خطا مواجه خواهیم شد.

ارسال دیتا به API بدون کلید name
دریافت خطا با کد 500

همانطور که مشاهده کردید، کلید name را در دیتای ارسالی خود به API مشخص نکردیم و با خطایی با کد 500 و پیام Mapping key not found. روبرو شدیم.

GET

در این متد تنها نیاز داریم که id نامی که می‌خواهیم مقدار آن را دریافت کنیم را در درخواست ارسال کنیم، در پاسخ اگر شخصی با چنین id وجود داشته باشد، کد وضعیت و نام آن شخص را دریافت خواهیم کرد.

ارسال درخواست GET با مقدار id = 1
پاسخ درخواست قبل

خطا در درخواست GET

در نظر بگیرید که شخصی با id برابر 2 نداشته باشیم. اگر درخواستی برای دریافت اطلاعات شخص با id = 2 کنیم، با خطا روبرو خواهیم شد.

ارسال درخواست GET با مقدار id = 2
دریافت خطا با کد 500

از آنجایی که چنین شخصی با id مشخص شده وجود نداشت، به خطا با کد 500 و پیام Mapping key not found. برخورد کردیم.

جمع‌بندی

در این مقاله ایجاد یک API توسط Flask و Flask-RESTPlus را بررسی کردیم. هر دو این ماژول‌ها، موارد مناسبی برای ساخت APIهای مستندسازی‌شده توسط پایتون است که در عین حال بتوان از Swagger UI استفاده کرد.

منبع: https://towardsdatascience.com/working-with-apis-using-flask-flask-restplus-and-swagger-ui-7cf447deda7f