بناء واجهة برمجة تطبيقات الراحة باستخدام إطار الزجاجة
نشرت: 2022-03-11أصبحت واجهات برمجة تطبيقات REST طريقة شائعة لإنشاء واجهة بين الواجهات الخلفية للويب والواجهات الأمامية ، وبين خدمات الويب المختلفة. إن بساطة هذا النوع من الواجهة والدعم الشامل لبروتوكولات HTTP و HTTPS عبر شبكات وأطر عمل مختلفة تجعله خيارًا سهلاً عند التفكير في مشكلات التشغيل البيني.
الزجاجة عبارة عن إطار عمل ويب بسيط بلغة Python. إنه خفيف الوزن وسريع وسهل الاستخدام ومناسب تمامًا لبناء خدمات RESTful. وضعت المقارنة المجردة التي أجراها Andriy Kornatskyy ضمن أفضل ثلاثة أطر من حيث وقت الاستجابة والإنتاجية (الطلبات في الثانية). في اختباراتي الخاصة على الخوادم الافتراضية المتاحة من DigitalOcean ، وجدت أن الجمع بين مكدس خادم uWSGI وزجاجة يمكن أن يحقق تكلفة منخفضة تصل إلى 140 ميكرو ثانية لكل طلب.
في هذه المقالة ، سأقدم شرحًا تفصيليًا لكيفية إنشاء خدمة RESTful API باستخدام Bottle.
التثبيت والتكوين
يحقق إطار الزجاجة أدائه المثير للإعجاب جزئيًا بفضل وزنه الخفيف. في الواقع يتم توزيع المكتبة بأكملها كوحدة نمطية ذات ملف واحد. هذا يعني أنه لا يمسك بيدك مثل الأطر الأخرى ، ولكنه أيضًا أكثر مرونة ويمكن تكييفه ليلائم العديد من مجموعات التكنولوجيا المختلفة. لذلك فإن الزجاجة هي الأنسب للمشاريع التي يكون فيها الأداء وقابلية التخصيص أعلى من قيمتها ، وحيث تكون مزايا توفير الوقت لأطر العمل الشاقة أقل أهمية.
تجعل مرونة Bottle وصفًا متعمقًا لإعداد النظام الأساسي غير مجدي بعض الشيء ، نظرًا لأنه قد لا يعكس مجموعتك الخاصة. ومع ذلك ، فإن نظرة عامة سريعة على الخيارات ، ومكان معرفة المزيد حول كيفية إعدادها ، مناسبة هنا:
تثبيت
يعد تثبيت Bottle سهلاً مثل تثبيت أي حزمة Python أخرى. خياراتك هي:
- قم بالتثبيت على نظامك باستخدام مدير حزم النظام. يحزم Debian Jessie (المستقر الحالي) الإصدار 0.12 على شكل زجاجة بيثون .
- قم بالتثبيت على نظامك باستخدام فهرس حزمة Python مع
pip install bottle
. - التثبيت في بيئة افتراضية (موصى به).
لتثبيت Bottle على بيئة افتراضية ، ستحتاج إلى أدوات virtualenv و pip . لتثبيتها ، يرجى الرجوع إلى وثائق virtualenv و pip ، على الرغم من أنك ربما تكون موجودة على نظامك بالفعل.
في Bash ، أنشئ بيئة باستخدام Python 3:
$ virtualenv -p `which python3` env
سيؤدي كبح المعلمة -p `which python3`
إلى تثبيت مترجم Python الافتراضي الموجود على النظام - عادةً Python 2.7. يتم دعم Python 2.7 ، لكن هذا البرنامج التعليمي يفترض Python 3.4.
الآن قم بتنشيط البيئة وقم بتثبيت Bottle:
$ . env/bin/activate $ pip install bottle
هذا هو. الزجاجة مثبتة وجاهزة للاستخدام. إذا لم تكن معتادًا على virtualenv أو pip ، فإن وثائقهم من الدرجة الأولى. إلق نظرة! فهي تستحق ذلك.
الخادم
تتوافق الزجاجة مع واجهة بوابة خادم الويب القياسية (WSGI) من Python ، مما يعني أنه يمكن استخدامها مع أي خادم متوافق مع WSGI. يتضمن ذلك uWSGI و Tornado و Gunicorn و Apache و Amazon Beanstalk و Google App Engine وغيرها.
تختلف الطريقة الصحيحة لإعداده اختلافًا طفيفًا مع كل بيئة. تعرض الزجاجة كائنًا يتوافق مع واجهة WSGI ، ويجب تكوين الخادم للتفاعل مع هذا الكائن.
لمعرفة المزيد حول كيفية إعداد الخادم الخاص بك ، ارجع إلى مستندات الخادم ، ومستندات Bottle ، هنا.
قاعدة البيانات
الزجاجة حيادية بقاعدة البيانات ولا تهتم بمصدر البيانات. إذا كنت ترغب في استخدام قاعدة بيانات في تطبيقك ، فإن Python Package Index يحتوي على العديد من الخيارات المثيرة للاهتمام ، مثل SQLAlchemy و PyMongo و MongoEngine و CouchDB و Boto لـ DynamoDB. ما عليك سوى المحول المناسب لجعله يعمل مع قاعدة البيانات التي تختارها.
أساسيات إطار الزجاجة
الآن ، دعنا نرى كيفية إنشاء تطبيق أساسي في Bottle. بالنسبة لأمثلة الكود ، سأفترض أن Python> = 3.4. ومع ذلك ، فإن معظم ما سأكتبه هنا سيعمل على Python 2.7 أيضًا.
يبدو التطبيق الأساسي في Bottle كما يلي:
import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
عندما أقول أساسي ، أعني أن هذا البرنامج لا يعني حتى "Hello World" أنت. (متى كانت آخر مرة قمت فيها بالوصول إلى واجهة REST التي أجابت على "Hello World؟") ستتلقى جميع طلبات HTTP إلى 127.0.0.1:8000
حالة استجابة 404 Not Found.
تطبيقات في زجاجة
قد تحتوي الزجاجة على العديد من التطبيقات التي تم إنشاؤها ، ولكن من أجل الراحة ، تم إنشاء المثال الأول لك ؛ هذا هو التطبيق الافتراضي. تحافظ الزجاجة على هذه الحالات في مكدس داخلي للوحدة. عندما تفعل شيئًا باستخدام Bottle (مثل تشغيل التطبيق أو إرفاق مسار) ولا تحدد التطبيق الذي تتحدث عنه ، فهذا يشير إلى التطبيق الافتراضي. في الواقع ، لا يلزم وجود سطر app = application = bottle.default_app()
في هذا التطبيق الأساسي ، ولكنه موجود حتى نتمكن من استدعاء التطبيق الافتراضي بسهولة باستخدام Gunicorn أو uWSGI أو خادم WSGI عام.
قد تبدو إمكانية وجود تطبيقات متعددة مربكة في البداية ، لكنها تضيف مرونة إلى Bottle. بالنسبة للوحدات النمطية المختلفة للتطبيق الخاص بك ، يمكنك إنشاء تطبيقات زجاجة متخصصة عن طريق إنشاء فئات أخرى من الزجاجة وإعدادها باستخدام تكوينات مختلفة حسب الحاجة. يمكن الوصول إلى هذه التطبيقات المختلفة من خلال عناوين URL مختلفة ، من خلال جهاز توجيه URL الخاص بـ Bottle. لن نتعمق في ذلك في هذا البرنامج التعليمي ، ولكن نشجعك على أخذ نظرة على وثائق Bottle هنا وهنا.
استدعاء الخادم
يقوم السطر الأخير من البرنامج النصي بتشغيل Bottle باستخدام الخادم المشار إليه. إذا لم تتم الإشارة إلى أي خادم ، كما هو الحال هنا ، فإن الخادم الافتراضي هو الخادم المرجعي WSGI المدمج في Python ، وهو مناسب فقط لأغراض التطوير. يمكن استخدام خادم مختلف مثل هذا:
bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)
هذا هو السكر النحوي الذي يتيح لك بدء تشغيل التطبيق عن طريق تشغيل هذا البرنامج النصي. على سبيل المثال ، إذا تم تسمية هذا الملف باسم main.py
، فيمكنك ببساطة تشغيل python main.py
لبدء التطبيق. تحتوي الزجاجة على قائمة واسعة جدًا من محولات الخادم التي يمكن استخدامها بهذه الطريقة.
لا تحتوي بعض خوادم WSGI على محولات زجاجة. يمكن أن تبدأ بأوامر التشغيل الخاصة بالخادم. على uWSGI ، على سبيل المثال ، كل ما عليك فعله هو استدعاء uwsgi
مثل هذا:
$ uwsgi --http :8000 --wsgi-file main.py
ملاحظة حول بنية الملف
تترك الزجاجة بنية ملفات التطبيق بالكامل متروكة لك. لقد وجدت أن سياسات بنية الملفات الخاصة بي تتطور من مشروع إلى آخر ، ولكنها تميل إلى الاستناد إلى فلسفة MVC.
بناء واجهة برمجة تطبيقات REST الخاصة بك
بالطبع ، لا أحد يحتاج إلى خادم يقوم بإرجاع 404 فقط لكل URI مطلوب. لقد وعدتك ببناء واجهة برمجة تطبيقات REST ، لذلك دعونا نفعل ذلك.
لنفترض أنك ترغب في إنشاء واجهة تعالج مجموعة من الأسماء. في التطبيق الحقيقي ، من المحتمل أن تستخدم قاعدة بيانات لهذا ، ولكن في هذا المثال ، سنستخدم فقط بنية البيانات set
في الذاكرة.
قد يبدو الهيكل العظمي لواجهة برمجة التطبيقات لدينا هكذا. يمكنك وضع هذا الرمز في أي مكان في المشروع ، لكن توصيتي ستكون ملف واجهة برمجة تطبيقات منفصل ، مثل api/names.py
.
from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass
التوجيه
كما نرى ، يتم التوجيه في الزجاجة باستخدام أدوات الديكور. يقوم المصممون المستوردون post
معالجات التسجيل get
put
delete
لهذه الإجراءات الأربعة. فهم كيف يمكن تقسيم هذه الأعمال على النحو التالي:
- جميع الديكورات المذكورة أعلاه هي اختصار
default_app
التوجيه الافتراضي. على سبيل المثال ، يطبق مصمم الديكورget (@get()
bottle.default_app().get()
على المعالج. - طرق التوجيه في
default_app
كلها اختصاراتroute()
. لذا ، فإنdefault_app().get('/')
يعادلdefault_app().route(method='GET', '/')
.
إذن @get('/')
هو نفسه @route(method='GET', '/')
، وهو نفس @bottle.default_app().route(method='GET', '/')
، ويمكن استخدامها بالتبادل.
أحد الأشياء المفيدة في مصمم @route
هو أنه إذا كنت ترغب ، على سبيل المثال ، في استخدام نفس المعالج للتعامل مع كل من تحديثات الكائن وحذفه ، فيمكنك فقط تمرير قائمة بالطرق التي يتعامل معها مثل هذا:
@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass
حسنًا ، دعنا نطبق بعض هذه المعالجات.
ما بعد: إنشاء الموارد
قد يبدو معالج POST كما يلي:
import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})
حسنًا ، هذا كثير جدًا. دعونا نراجع هذه الخطوات جزءًا تلو الآخر.
اعراب الجسم
تتطلب واجهة برمجة التطبيقات هذه من المستخدم أن ينشر سلسلة JSON في الجسم بسمة تسمى "الاسم".
يشير كائن request
الذي تم استيراده مسبقًا من bottle
دائمًا إلى الطلب الحالي ويحتفظ بجميع بيانات الطلب. تحتوي سمة نصه على دفق بايت من body
الطلب ، والذي يمكن الوصول إليه بواسطة أي وظيفة قادرة على قراءة كائن دفق (مثل قراءة ملف).

يتحقق أسلوب request.json()
من رؤوس الطلب لنوع المحتوى "application / json" ويوزع الجسم إذا كان صحيحًا. إذا اكتشف Bottle جسمًا مشوهًا (على سبيل المثال: فارغ أو به نوع محتوى خاطئ) ، فإن هذه الطريقة تُرجع None
وبالتالي نرفع قيمة ValueError
. إذا تم اكتشاف محتوى JSON تالف بواسطة محلل JSON ؛ إنه يثير استثناءً ValueError
، مرة أخرى باعتباره خطأ في القيمة.
تحليل الكائن والتحقق من صحته
إذا لم تكن هناك أخطاء ، فقد قمنا بتحويل نص الطلب إلى كائن Python المشار إليه بواسطة متغير data
. إذا تلقينا قاموسًا به مفتاح "اسم" ، فسنكون قادرين على الوصول إليه عبر data['name']
. إذا تلقينا قاموسًا بدون هذا المفتاح ، فإن محاولة الوصول إليه ستقودنا إلى استثناء KeyError
. إذا تلقينا أي شيء بخلاف القاموس ، فسنحصل على استثناء من TypeError
. في حالة حدوث أي من هذه الأخطاء ، مرة أخرى ، نعيد ValueError
في القيمة ، مما يشير إلى إدخال غير صحيح.
للتحقق مما إذا كان مفتاح الاسم يحتوي على التنسيق الصحيح ، يجب علينا اختباره مقابل قناع regex ، مثل قناع نمط الاسم الذي namepattern
هنا. إذا لم يكن name
المفتاح سلسلة ، namepattern.match()
سيرفع خطأ TypeError
، وإذا لم يكن متطابقًا ، فسيعود None
.
باستخدام القناع في هذا المثال ، يجب أن يكون الاسم أبجديًا رقميًا ASCII بدون فراغات من 1 إلى 64 حرفًا. هذه عملية تحقق بسيطة ولا تختبر لكائن به بيانات غير صحيحة ، على سبيل المثال. يمكن تحقيق التحقق الأكثر تعقيدًا وكاملة من خلال استخدام أدوات مثل FormEncode.
اختبار للوجود
الاختبار الأخير قبل تنفيذ الطلب هو ما إذا كان الاسم المحدد موجودًا بالفعل في المجموعة. في تطبيق أكثر تنظيماً ، من المحتمل أن يتم إجراء هذا الاختبار بواسطة وحدة مخصصة ويتم إرسال إشارة إلى واجهة برمجة التطبيقات الخاصة بنا من خلال استثناء متخصص ، ولكن نظرًا لأننا نتعامل مع مجموعة بشكل مباشر ، يتعين علينا القيام بذلك هنا.
نشير إلى وجود الاسم عن طريق رفع KeyError
.
الردود على الخطأ
مثلما يحتفظ كائن الطلب بجميع بيانات الطلب ، فإن كائن الاستجابة يفعل الشيء نفسه لبيانات الاستجابة. هناك طريقتان لتعيين حالة الاستجابة:
response.status = 400
و:
response.status = '400 Bad Request'
على سبيل المثال ، اخترنا النموذج الأبسط ، ولكن يمكن استخدام النموذج الثاني لتحديد وصف نص الخطأ. داخليًا ، ستقوم Bottle بتقسيم السلسلة الثانية وتعيين الرمز الرقمي بشكل مناسب.
استجابة النجاح
إذا نجحت جميع الخطوات ، فإننا نلبي الطلب عن طريق إضافة الاسم إلى مجموعة _names
، وتعيين رأس استجابة Content-Type
، وإعادة الاستجابة. سيتم التعامل مع أي سلسلة يتم إرجاعها بواسطة الدالة على أنها نص استجابة لاستجابة 200 Success
، لذلك نقوم ببساطة بإنشاء واحدة باستخدام json.dumps
.
احصل على: قائمة الموارد
بالانتقال من إنشاء الاسم ، سنقوم بتنفيذ معالج قائمة الاسم:
@get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})
كان سرد الأسماء أسهل بكثير ، أليس كذلك؟ بالمقارنة مع إنشاء الاسم ، ليس هناك الكثير للقيام به هنا. ما عليك سوى تعيين بعض رؤوس الردود وإرجاع تمثيل JSON لجميع الأسماء ، وقد انتهينا.
PUT: تحديث المورد
الآن ، دعنا نرى كيفية تنفيذ طريقة التحديث. إنها لا تختلف كثيرًا عن طريقة الإنشاء ، لكننا نستخدم هذا المثال لتقديم معلمات URI.
@put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})
مخطط الجسم لإجراء التحديث هو نفسه بالنسبة لإجراء الإنشاء ، ولكن لدينا الآن أيضًا معلمة oldname
الجديدة في URI ، كما هو محدد بواسطة المسار @put('/names/<oldname>')
.
معلمات URI
كما ترى ، فإن تدوين Bottle معلمات URI واضح جدًا. يمكنك بناء محددات مواقع المعلومات (URIs) باستخدام العديد من المعلمات كما تريد. تقوم الزجاجة باستخراجها تلقائيًا من URI وتمريرها إلى معالج الطلب:
@get('/<param1>/<param2>') def handler(param1, param2): pass
باستخدام أدوات تزيين المسار المتتالية ، يمكنك إنشاء URIs بمعلمات اختيارية:
@get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass
أيضًا ، تسمح Bottle لمرشحات التوجيه التالية في URIs:
-
int
يطابق فقط المعلمات التي يمكن تحويلها إلى
int
، ويمرر القيمة المحولة إلى المعالج:@get('/<param:int>') def handler(param): pass
-
float
مثل
int
، لكن بقيم الفاصلة العائمة:@get('/<param:float>') def handler(param): pass
-
re
(التعبيرات العادية)
يطابق فقط المعلمات التي تطابق التعبير العادي المحدد:
@get('/<param:re:^[az]+$>') def handler(param): pass
-
path
يطابق الأجزاء الفرعية لمسار URI بطريقة مرنة:
@get('/<param:path>/id>') def handler(param): pass
اعواد الكبريت:
/x/id
، تمريرx
على أنهاparam
./x/y/id
، تمريرx/y
param
.
حذف: حذف المورد
مثل طريقة GET ، فإن طريقة DELETE تجلب لنا القليل من الأخبار. لاحظ فقط أن إرجاع None
بدون تعيين حالة يؤدي إلى إرجاع استجابة بجسم فارغ ورمز حالة 200.
@delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return
الخطوة النهائية: تفعيل الـ API
لنفترض أننا حفظنا واجهة برمجة تطبيقات الأسماء الخاصة بنا باسم api/names.py
، يمكننا الآن تمكين هذه المسارات في ملف التطبيق الرئيسي main.py
import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
لاحظ أننا قمنا باستيراد وحدة names
فقط. نظرًا لأننا قمنا بتزيين جميع الطرق بمعرفات الموارد المنتظمة (URIs) الخاصة بها المرفقة بالتطبيق الافتراضي ، فلا داعي للقيام بأي إعداد إضافي. طرقنا موجودة بالفعل وجاهزة للوصول إليها.
يمكنك استخدام أدوات مثل Curl أو Postman لاستهلاك واجهة برمجة التطبيقات واختبارها يدويًا. (إذا كنت تستخدم Curl ، فيمكنك استخدام مُنسق JSON لجعل الاستجابة تبدو أقل تشوشًا.)
المكافأة: مشاركة الموارد عبر الأصول (CORS)
أحد الأسباب الشائعة لبناء واجهة برمجة تطبيقات REST هو الاتصال بواجهة JavaScript الأمامية من خلال AJAX. بالنسبة لبعض التطبيقات ، يجب السماح لهذه الطلبات أن تأتي من أي مجال ، وليس فقط المجال الرئيسي لواجهة برمجة التطبيقات الخاصة بك. بشكل افتراضي ، لا تسمح معظم المتصفحات بهذا السلوك ، لذا دعني أوضح لك كيفية إعداد مشاركة الموارد عبر الأصل (CORS) في Bottle للسماح بما يلي:
from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return
يسمح لنا مصمم hook
باستدعاء وظيفة قبل أو بعد كل طلب. في حالتنا ، لتمكين CORS ، يجب علينا تعيين رؤوس التحكم في Access-Control-Allow-Origin
بالأصل ، و -Allow-Methods
و -Allow-Headers
لكل رد من ردودنا. تشير هذه إلى مقدم الطلب أننا سنخدم الطلبات المشار إليها.
أيضًا ، قد يقوم العميل بإجراء طلب OPTIONS HTTP للخادم لمعرفة ما إذا كان بإمكانه بالفعل تقديم طلبات بطرق أخرى. باستخدام نموذج تجميع الكل هذا ، نستجيب لجميع طلبات OPTIONS برمز حالة 200 ونص فارغ.
لتمكين هذا ، ما عليك سوى حفظه واستيراده من الوحدة الرئيسية.
يتم إحتوائه
هذا كل ما في الامر!
مع هذا البرنامج التعليمي ، حاولت تغطية الخطوات الأساسية لإنشاء واجهة برمجة تطبيقات REST لتطبيق Python باستخدام إطار عمل ويب Bottle.
يمكنك تعميق معرفتك بهذا الإطار الصغير ولكن القوي من خلال زيارة البرنامج التعليمي والمستندات المرجعية لواجهة برمجة التطبيقات.