Skip to content

22. خدمات الويب باستخدام إطار العمل Flask

يُقصد بخدمة الويب هنا أي تطبيق ويب يقدم بيانات خام يستخدمها العميل، وغالبًا ما يكون برنامج نصي يعمل على وحدة التحكم في الأمثلة التالية. لا نهتم بتقنية معينة، مثل REST (REpresentational State Transfer) أو SOAP (Simple Object Access Protocol) على سبيل المثال، التي تقدم بيانات خام إلى حد ما بتنسيق محدد جيدًا. تقدم REST تنسيق jSON، بينما تقدم SOAP تنسيق XML. تصف كل تقنية من هذه التقنيات بدقة الطريقة التي يجب أن يستعلم بها العميل عن الخادم والشكل الذي يجب أن تتخذه استجابة الخادم. في هذه الدورة التدريبية، سنكون أكثر مرونة فيما يتعلق بطبيعة طلب العميل وطبيعة استجابة الخادم. ومع ذلك، فإن البرامج النصية المكتوبة والأدوات المستخدمة قريبة من تلك الخاصة بتقنية REST.

22.1. مقدمة

يمكن تنفيذ البرامج النصية المكتوبة بلغة Python بواسطة خادم ويب. ويصبح هذا البرنامج النصي برنامج خادم قادرًا على خدمة عدة عملاء. من وجهة نظر العميل، فإن استدعاء خدمة ويب يعادل طلب URL لتلك الخدمة. يمكن كتابة العميل بأي لغة برمجة، ولا سيما لغة Python. وفي هذه الحالة الأخيرة، يتم استخدام وظائف الإنترنت التي تناولناها للتو. كما يتعين علينا أن نعرف كيفية «التواصل» مع خدمة ويب، أي فهم بروتوكول HTTP للاتصال بين خادم الويب وعملائه. كان هذا هو الهدف من الفقرة |بروتوكول HTTP|. وقد مكنتنا برامج العميل الويب الموصوفة في هذا الجزء من الدورة التدريبية من اكتشاف جزء من بروتوكول HTTP.

Image

في أبسط صورها، تتم عمليات التبادل بين العميل والخادم على النحو التالي:

  • يقوم العميل بفتح اتصال مع المنفذ 80 لخادم الويب؛
  • يُرسل طلبًا للحصول على مستند؛
  • يرسل خادم الويب المستند المطلوب ويغلق الاتصال؛
  • ثم يقوم العميل بدوره بإغلاق الاتصال؛

يمكن أن يكون المستند من أنواع مختلفة: نص بتنسيق HTML، أو صورة، أو مقطع فيديو، ... وقد يكون مستندًا موجودًا مسبقًا (مستند ثابت) أو مستندًا يتم إنشاؤه على الفور بواسطة برنامج نصي (مستند ديناميكي). في الحالة الأخيرة، نُطلق على ذلك اسم «برمجة الويب». يمكن كتابة البرنامج النصي الخاص بإنشاء المستندات ديناميكيًا بعدة لغات برمجة: PHP، وPython، وPerl، وJava، وRuby، وC#، وVB.net، ...

فيما يلي، سنستخدم نصوص برمجية بلغة Python لإنشاء مستندات نصية ديناميكيًا.

Image

  • في [1]، يفتح العميل اتصالاً مع الخادم، ويطلب برنامجًا نصيًّا بلغة Python، ويرسل أو لا يرسل معلمات إلى هذا البرنامج النصي؛
  • في [3]، يقوم خادم الويب بتنفيذ البرنامج النصي لـ Python بواسطة مترجم Python. يقوم البرنامج النصي بإنشاء مستند يتم إرساله إلى العميل [2]؛
  • يقوم الخادم بإنهاء الاتصال. ويقوم العميل بالمثل؛

يمكن لخادم الويب معالجة عدة عملاء في آن واحد.

فيما يلي، سنستخدم خادمين للويب:

  • خادم Werkzeug الخفيف [https://werkzeug.palletsprojects.com/en/1.0.x/]. يستخدم هذا الخادم إطار عمل الويب Flask [https://flask.palletsprojects.com/en/1.1.x/]. سنطلق عليه في الغالب اسم خادم Flask؛
  • خادم Apache 2 [https://httpd.apache.org/

سيُستخدم خادم Flask في جميع الأمثلة. أما خادم Apache فسيُستخدم لاستضافة تطبيق الويب الذي سنقوم بتطويره.

تم تطوير إطار عمل Flask بلغة Python. وهو عبارة عن وحدة نمطية يتم تثبيتها في محطة طرفية PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask
Collecting flask
  Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
     || 94 kB 1.1 MB/s
Collecting click>=5.1
  Downloading click-7.1.2-py2.py3-none-any.whl (82 kB)
     || 82 kB 5.8 MB/s
Collecting itsdangerous>=0.24
  Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting Jinja2>=2.10.1
  Downloading Jinja2-2.11.2-py2.py3-none-any.whl (125 kB)
     || 125 kB 6.4 MB/s
Collecting Werkzeug>=0.15
  Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
     || 298 kB 6.4 MB/s
Collecting MarkupSafe>=0.23
  Downloading MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl (16 kB)
Installing collected packages: click, itsdangerous, MarkupSafe, Jinja2, Werkzeug, flask
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.2 flask-1.1.2 itsdangerous-1.1.0
  • السطر 1: الأمر الذي تم تنفيذه؛
  • السطر 19: العناصر التي تم تثبيتها:
    • [flask-1.1.2]: هي إطار عمل لتطوير الويب بلغة Python؛
    • [Werkzeug-1.0.1]: هو خادم الويب الذي سيستجيب لطلبات العملاء؛
    • [Jinja2-2.11.2]: هي أداة تسمح بإدراج عناصر ديناميكية في صفحات كانت لتكون صفحات ثابتة لولا ذلك؛

22.2. نصوص برمجية [flask/01]: العناصر الأولى للبرمجة على الويب

Image

سيتم تنفيذ أمثلةنا في البنية التالية:

Image

  • في [1]، سيتم تنفيذ برنامج نصي بلغة Python كما يتم تنفيذ برنامج نصي تقليدي في وحدة التحكم؛
  • في [2]، يتم إنشاء مثيل لخادم ويب بشكل شفاف وينتظر الطلبات. في الواقع، لن يقبل سوى طلب واحد URL؛
  • في [3]، سيطلب المتصفح من الخادم طلبه الوحيد URL؛
  • في [4]، سيقوم الخادم بتنفيذ البرنامج النصي Python المحدد بواسطة وحدة التحكم [1]؛
  • في [5]، سيقوم البرنامج النصي بإرجاع نتائجه إلى خادم الويب، وهي مستند نصي؛
  • في [6]، سيقوم خادم الويب بإرسال هذا المستند النصي إلى المتصفح؛

22.2.1. البرنامج النصي [exemple_01]: أساسيات لغة HTML

يمكن لمتصفح الويب عرض مستندات متنوعة، وأكثرها شيوعًا هو مستند HTML (لغة الترميز HyperText). وهو عبارة عن نص منسق بعلامات على شكل <balise>texte</balise>. وبالتالي، فإن النص <b>important</b> سيعرض النص المهم بخط عريض. وهناك علامات منفردة، مثل العلامة <hr/> التي تعرض خطًا أفقيًّا. ولن نستعرض العلامات التي يمكن العثور عليها في نص HTML. توجد العديد من برامج WYSIWYG التي تتيح إنشاء صفحة WEB دون كتابة سطر واحد من كود HTML. تقوم هذه الأدوات تلقائيًّا بإنشاء كود HTML لتصميم الصفحة الذي يتم إنشاؤه باستخدام الماوس وعناصر التحكم المحددة مسبقًا. وبذلك يمكن إدراج (باستخدام الماوس) جدولًا في الصفحة، ثم الاطلاع على كود HTML الذي أنشأه البرنامج لمعرفة العلامات التي يجب استخدامها لتعريف جدول في صفحة WEB. الأمر ليس أكثر تعقيدًا من ذلك. من ناحية أخرى، فإن معرفة لغة HTML أمر لا غنى عنه، حيث يتعين على تطبيقات الويب الديناميكية أن تولد بنفسها كود HTML لإرساله إلى عملاء الويب. يتم إنشاء هذا الكود برمجيًا، ومن الضروري بالطبع معرفة ما يجب إنشاؤه حتى يحصل العميل على صفحة الويب التي يرغب فيها.

باختصار، ليس هناك حاجة لمعرفة لغة HTML بالكامل لبدء البرمجة على الويب. ومع ذلك، فإن هذه المعرفة ضرورية ويمكن اكتسابها من خلال استخدام برامج WYSIWYG لإنشاء صفحات الويب WEB مثل DreamWeaver وعشرات البرامج الأخرى. هناك طريقة أخرى لاكتشاف خفايا لغة HTML، وهي تصفح الويب وعرض الكود المصدري للصفحات التي تتميز بخصائص مثيرة للاهتمام وما زالت غير معروفة بالنسبة لك.

لنأخذ المثال التالي الذي يعرض بعض العناصر التي يمكن العثور عليها في مستند ويب مثل:

  • جدول؛
  • صورة؛
  • رابط؛

Image

يُحاط المستند HTML بعلامات <html>…</html>. ويتكون من جزأين:

  • <head>…</head>: هذا هو الجزء غير القابل للعرض من المستند. وهو يوفر معلومات للمتصفح الذي سيقوم بعرض المستند. وغالبًا ما نجد فيه العلامة <title>…</title> التي تحدد النص الذي سيظهر في شريط عنوان المتصفح. وقد نجد فيه علامات أخرى، لا سيما العلامات التي تحدد الكلمات المفتاحية للمستند، وهي الكلمات المفتاحية التي تستخدمها محركات البحث لاحقًا. كما يمكن العثور في هذا الجزء على نصوص برمجية، مكتوبة غالبًا بلغة جافا سكريبت أو VBScript، والتي سيتم تنفيذها بواسطة المتصفح؛
  • <body attributs>…</body>: هذا هو الجزء الذي سيعرضه المتصفح. تشير العلامات HTML الموجودة في هذا الجزء إلى المتصفح الشكل المرئي «المطلوب» للوثيقة. سيقوم كل متصفح بتفسير هذه العلامات بطريقته الخاصة. وبالتالي، قد يعرض متصفحان نفس المستند على الويب بشكل مختلف. وهذا عادةً ما يمثل أحد التحديات التي يواجهها مصممو الويب؛

الرمز HTML لمستندنا النموذجي هو كما يلي:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Quelques balises HTML</title>
</head>

<body style="background-image: url(/static/images/standard.jpg)">
  <h1 style="text-align: left">Quelques balises HTML</h1>
  <hr />

  <table border="1">
    <thead>
      <tr>
        <th>Colonne 1</th>
        <th>Colonne 2</th>
        <th>Colonne 3</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>cellule(1,1)</td>
        <td style="text-align: center;">cellule(1,2)</td>
        <td>cellule(1,3)</td>
      </tr>
      <tr>
        <td>cellule(2,1)</td>
        <td>cellule(2,2)</td>
        <td>cellule(2,3</td>
      </tr>
    </tbody>
  </table>
  <br /><br />
  <table border="0">
    <tr>
      <td>Une image</td>
      <td>
        <img border="0" src="/static/images/cerisier.jpg" />
      </td>
    </tr>
    <tr>
      <td>Le site de Polytech'Angers</td>
      <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
    </tr>
  </table>
</body>
</html>
العناصر
العلامات والأمثلة HTML
titre du document
<title>بعض العلامات HTML</title> (السطر 5)
سيظهر النص [Quelques balises HTML] في شريط عنوان المتصفح الذي سيعرض المستند
barre horizontale
<hr />: يعرض خطًا أفقيًا (السطر 10)
tableau
<سمات الجدول>….</table>: لتعريف الجدول (السطران 12 و32)
<thead>…</thead>: لتعريف عناوين الأعمدة (السطران 13 و19)
<tbody>…</tbody>: لتحديد محتوى الجدول (السطر 20، 31)
<tr attributs>…</tr>: لتعريف صف (السطران 21 و25)
<td سمات>…</td>: لتعريف خلية (السطر 22)
أمثلة:
<table border="1">…</table>: تحدد السمة border سماكة حدود الجدول
<td style="text-align: center;">خلية(1,2)</td> (السطر 23): تحدد خلية سيكون محتواها هو «خلية(1,2)». وسيتم توسيط هذا المحتوى أفقيًا (text-align: center).
image
<img border="0" src="/static/images/cerisier.jpg"/> (السطر 38): يحدد صورة بدون حدود (border="0")، وملفها المصدر هو [/static/images/cerisier.jpg] على خادم الويب (src="/static/images/cerisier.jpg"). إذا كان هذا الرابط موجودًا في مستند ويب تم الحصول عليه باستخدام URL [http://server/chemin/balises.html]، فسيطلب المتصفح URL [http://server/ static/images/cerisier.jpg] للحصول على الصورة المشار إليها هنا.
lien
<a href="http://www.polytech-angers.fr/fr/index.html">هنا</a> (السطر 43): يجعل النص ici بمثابة رابط إلى URL http://www.polytech-angers.fr/fr/index.html.
fond de page
<body style="background-image: url(/static/images/standard.jpg)"> (السطر 8): يشير إلى أن الصورة التي يجب أن تُستخدم كخلفية للصفحة موجودة في URL [/static/images/standard.jpg] على خادم الويب. في سياق مثالنا هذا، سيطلب المتصفح ملف URL و[http://server/static/images/standard.jpg] للحصول على صورة الخلفية هذه.

ونلاحظ في هذا المثال البسيط أنه لإنشاء المستند بالكامل، يتعين على المتصفح إرسال ثلاث طلبات إلى الخادم:

  • [http://server/chemin/balises.html] للحصول على مصدر المستند HTML؛
  • [http://server/static/images/cerisier.jpg] للحصول على الصورة cerisier.jpg؛
  • [http://server/static/images/standard.jpg] للحصول على صورة الخلفية standard.jpg؛

سيسمح لنا البرنامج النصي [exemple_01] بعرض الصفحة الثابتة السابقة [balises.html]:

Image

  • في [1]، البرنامج النصي [exemple_01] الذي سيتم تنفيذه؛
  • في [3]، المستند HTML الذي سيتم عرضه بواسطة البرنامج النصي؛
  • في [2]، الصور الموجودة في المستند HTML؛

النص البرمجي [exemple_01] هو كما يلي:


import os

from flask import Flask, make_response, render_template

# تطبيق Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # عرض الصفحة
    return make_response(render_template("balises.html"))


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 7: يتم إنشاء مثيل لتطبيق Flask. تطبيق Flask هو تطبيق ويب؛
    • المعلمة الأولى هي الاسم الممنوح للتطبيق. يمكن تسميته بأي اسم نريده. هنا استخدمنا السمة المحددة مسبقًا [__name__] التي تساوي [__main__] (السطر 18)؛
    • المعلمة الثانية هي معلمة مسماة، أي أن ترتيبها ضمن قائمة المعلمات لا يهم. تشير المعلمة المسماة [template_folder] إلى المجلد الذي توجد فيه الصفحات الثابتة لتطبيق الويب. يتم إرسال الصفحات الثابتة إلى المتصفح كما هي. هنا، سيتم العثور على الصفحات الثابتة في المجلد [templates] ضمن شجرة المشروع. في السطر 7، قمنا بتعيين مسار نسبي إلى المجلد [script_dir] الذي يحتوي على البرنامج النصي [exemple_01] الذي يتم تنفيذه؛
    • كما أن المعلمة الثالثة هي أيضًا معلمة مسماة. يشير [static_folder] إلى المجلد الذي سنجد فيه موارد المستند HTML (الصور، مقاطع الفيديو، ...). وهنا أيضًا، قمنا بتحديد مسار نسبي للمجلد [script_dir] الذي يحتوي على البرنامج النصي [exemple_01] الذي تم تنفيذه؛
  • الأسطر 10-14: يتم تحديد ملفات URL التي يقبلها تطبيق الويب. ترتبط كل ملف URL بوظيفة يتم تنفيذها عند طلب ملف URL بواسطة متصفح ويب؛
  • السطر 11: URL الوحيد في التطبيق هو URL [/]. لاحظ أن في [@app.route('/')]، فإن [app] هي المتغير الذي تم تهيئته في السطر 7. وبالتالي، فإن تعريف المسارات (المسارات المختلفة التي يديرها التطبيق) يأتي بالضرورة بعد تعريف التطبيق. هذا الاسم الأخير يمكن اختياره بحرية؛
  • الأسطر 12-14: الدالة التي يتم تنفيذها عند طلب URL [/] من تطبيق الويب [exemple_01]؛
  • السطر 12: يمكن أن تحمل الدالة المرتبطة بـ URL أي اسم. وقد تحتوي أحيانًا على معلمات لاسترداد عناصر من URL المرتبطة بها. وهي هنا لا تحتوي على أي معلمات؛
  • السطر 14:
    • تُرجع الدالة [render_template] سلسلة أحرف تمثل المستند النصي الناتج عن معلمتها. والمعلمة هنا هي [balises.html]. بسبب وجود [template_folder] في السطر 7، سيتم البحث عن هذا المستند في المجلد [f"{script_dir}/../templates"]. وهو موجود بالفعل هناك؛
    • تقوم الدالة [make_response] بإنشاء استجابة HTTP للمتصفح الذي طلب منها URL [/]. وقد رأينا في الفقرة |بروتوكول HTTP| أن الاستجابة HTTP تتكون من عنصرين:
      • رؤوس HTTP؛
      • المستند المطلوب من قبل المتصفح، وهو في هذه الحالة مستند HTML؛

في السطر 14، لم يتم تزويد الدالة [make_response] بأي معلمات لإنشاء رؤوس HTTP. لذا ستقوم الدالة بإنشائها افتراضيًا. سنرى لاحقًا كيفية تعيين هذه الرؤوس HTTP.

  • أخيرًا، عندما يطلب المتصفح ملف URL من تطبيق Flask، فإنه يحصل على الصفحة [balises.html]؛
  • الأسطر 17-20: تُستخدم هذه الأسطر لتشغيل خادم الويب الذي سيقوم بتنفيذ تطبيق الويب [exemple_01]؛
    • السطر 18: لا تنطبق هذه الشرط إلا عندما يتم تشغيل البرنامج النصي [exemple_01] داخل وحدة التحكم؛
    • السطر 19: يتم تكوين التطبيق [app] المذكور في السطر 7:
    • المعلمة المسماة [ENV="development"] تضع خادم الويب في وضع التطوير: بمجرد أن يقوم المطور بتعديل عنصر من عناصر التطبيق، يتم إعادة إنشاء التطبيق وتسليمه إلى خادم الويب. ولا يحتاج المطور إلى طلب تشغيل جديد؛
    • المعلمة المسماة [DEBUG=True] ستسمح للمطور بوضع نقاط توقف في كود التطبيق؛
    • السطر 20: يتم تشغيل تطبيق الويب: يتم إنشاء مثيل لخادم الويب ونشر تطبيق الويب عليه من أجل الاستجابة لطلبات عملاء الويب؛

فيما يلي مثال على التنفيذ:

Image

تظهر السجلات التالية في وحدة التحكم الخاصة بالتنفيذ:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/flask/01/main/exemple_01.py
 * Serving Flask app "exemple_01" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (اضغط على CTRL+C للخروج)
  • السطر 2: يعرض الخادم البرنامج النصي الذي تم تنفيذه؛
  • السطر 3: نحن في وضع التطوير؛
  • السطران 4-5: يرى الخادم أنه تم تشغيله في وضع [debug]. ثم يعيد التشغيل (السطر 5). لذا، فإن وضع [debug] يبطئ عملية التشغيل قليلاً؛
  • السطر 8: URL حيث يتوفر تطبيق الويب الذي تم نشره [exemple_01]؛

باستخدام متصفح ويب، نطلب URL [http://127.0.0.1:5000/]:

Image

ونحصل بالفعل على المستند المتوقع [balises.html].

22.2.2. نص برمجي [exemple_02]: إنشاء مستند HTML ديناميكيًا

Image

سيقوم البرنامج النصي [exemple_02] [1] بإنشاء المستند [exemple_02.html] [2] التالي:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>{{page.title}}</title>
</head>
<body>
    <b>{{page.contents}}</b>
</body>
</html>

هذه الوثيقة ديناميكية لأن محتواها لا يُعرف بالكامل إلا في اللحظة التي يقوم فيها خادم الويب بتقديمها. ففي السطرين 5 و8، يوجد عنصران غير معروفين وقت كتابة الصفحة. ولا يُعرفان إلا في اللحظة التي يتم فيها إرسال الصفحة إلى العميل. وعندها يتم استبدالهما بقيمهما، وهي عبارة عن سلاسل أحرف.

  • السطران 5 و8: صيغة {{expression}} هي صيغة من لغة القوالب Jinja2 [https://jinja.palletsprojects.com/en/2.11.x/]. قبل إرسال الصفحة إلى العميل، يتم تقييم العناصر الديناميكية في الصفحة (السطران 5 و8) واستبدالها بقيمها؛
  • السطر 5: تم استخدام صيغة [page.title]. لذا، افترضنا أنه عند إنشاء الصفحة قبل إرسالها، تكون المتغير [page] معروفًا، وسنرى كيف. في صيغة {{expression}}، يمكن استخدام أسماء المتغيرات التي نريدها. في السطرين 5 و8، يمكننا استخدام {{title}} و{{contents}}. يمكننا القول إذن إن [title] و[contents] هما معلمتان للصفحة. في ما يلي، سنستخدم دائمًا نفس التقنية:
    • سيكون المعلمة الوحيدة للصفحة هي قاموس [page]؛
    • وستُستخدم سمات هذا القاموس في الصفحة. هنا [page.title] في السطر 5 و [page.contents] في السطر 8؛

تطبيق الويب [exemple_02.py] هو كما يلي:


from flask import Flask, make_response, render_template

# تطبيق Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # محتوى الصفحة في شكل قاموس
    page = {"title": "un titre", "contents": "un contenu"}
    # عرض الصفحة
    return make_response(render_template("exemple_02.html", page=page))


# الصفحة الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • لقد أوضحنا بالفعل في المثال السابق الأسطر 4-5 و18-20. وسنستمر في استخدام هذا النمط في أمثلةنا؛
  • السطر 9: الملف الوحيد URL الذي يقدمه تطبيق الويب هو URL
  • السطر 14: المستند الذي يتم تقديمه إلى URL / هو المستند [exemple_02.html] الذي قمنا بتعليقه للتو. ونعلم أنه يحتوي على معلمة واحدة، وهي قاموس يُسمى [page]؛
  • السطر 12: نُعرِّف القاموس الذي سيتم تمريره كمعلمة إلى الصفحة [exemple_02.html]. يمكن أن يحمل أي اسم. لكن يجب أن يحتوي على السمات [title, contents] المستخدمة في المستند HTML؛
  • السطر 14: تتمثل وظيفة الدالة [render_template] في تحويل سلسلة الأحرف الواردة في المستند [exemple_02.html]. ونظرًا لأن هذا المستند هو مستند معلم، فإننا نمرر إلى الدالة [render_template] المعلمة أو المعلمات المتوقعة. ونقوم بذلك هنا عن طريق تعيين قيمة للمعلمة المسماة [page]. في العملية [page=page]:
    • على يسار علامة =، يوجد المعلمة [page] المستخدمة في المستند [exemple_02.html]؛
    • على يمين علامة =، توجد القيمة [page] المحددة في السطر 12؛
    • بشكل عام، إذا كان المستند HTML يحتوي على المعلمات [param1, param2, …, paramn]، فسيتم تمرير قيمها إلى الدالة [render_template] في صيغة [render_template(document, param1=valeur1, param2=valeur2, …]؛

قبل تنفيذ [exemple_02]، يجب علينا إيقاف تنفيذ [exemple_01]:

Image

إذا شعرت أثناء تنفيذ البرنامج النصي 1 أن البرنامج النصي 2 هو الذي يتم تنفيذه، فربما يكون ذلك بسبب أن البرنامج النصي 2 لا يزال قيد التنفيذ. للعودة إلى حالة معروفة، يمكنك إيقاف جميع العمليات الجارية في PyCharm (في الجزء العلوي الأيمن من نافذة PyCharm):

Image

لنقم بتشغيل البرنامج النصي [exemple_02]:

Image

وستكون سجلات وحدة التحكم كما يلي:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/flask/01/main/exemple_02.py
 * Serving Flask app "exemple_02" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (اضغط على CTRL+C للخروج)

تشير السطر 8 إلى منفذ النشر (5000) لتطبيق [exemple_02] (السطر 1) على الجهاز [localhost]. وبما أن الأسطر السابقة لا تزال كما هي، فلن نعرضها مرة أخرى.

باستخدام متصفح، نطلب URL [http://localhost:5000/]:

Image

  • أنتجت العبارة {{page.title}} القيمة [1]؛
  • أنتجت العبارة {{page.contents}} القيمة [2]؛

22.2.3. نص برمجي [exemple_03]: استخدام أجزاء من الصفحة

Image

  • في [1]، سيقوم البرنامج النصي [exemple_03.py] بإنشاء المستند الديناميكي [exemple_03.html] [2]. وسيتم إنشاء هذا المستند من أجزاء الصفحة [fragment_01.html, fragment_02.html] و[3]؛

وسيكون المستند [exemple_03.html] كما يلي:


<!DOCTYPE html>
<html lang="fr">
{% include "fragments/fragment_01.html" %}
<body>
{% include "fragments/fragment_02.html" %}
</body>
</html>
  • في السطرين 3 و5، يتم استخدام توجيه Jinja2 [include] لتضمين عناصر خارجية في المستند؛
  • وصيغة الأمر هي {% include … %}. المعلمة الخاصة بالتوجيه [include] هي مسار المستند المراد تضمينه. هذا المسار نسبي بالنسبة للمعلمة [template_folder] لتطبيق Flask:

app = Flask(__name__, template_folder="../templates", static_folder="../static")

لذا، هنا، تُقاس مسارات المستندات بالنسبة إلى المجلد [templates].

المقطع [fragment_01.html] (الأسماء حرة بالطبع) هو التالي:


<meta charset="UTF-8">
<title>{{page.title}}</title>

الجزء [fragment_02.html] هو كما يلي:


<b>{{page.contents}}</b>

إذا قمنا بإعادة تكوين المستند [exemple_03.html] باستخدام هذه الأجزاء، نحصل على الكود التالي:


<!DOCTYPE html>
<html lang="fr">
<meta charset="UTF-8">
<title>{{page.title}}</title>
<body>
<b>{{page.contents}}</b>
</body>
</html>

وبذلك يكون لدينا مستند مطابق لـ [exemple_02.html] ولكنه مكون من أجزاء.

نص البرنامج النصي للويب [exemple_03.py] هو التالي:


import os

from flask import Flask, make_response, render_template

# تطبيق Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # محتوى الصفحة
    page = {"title": "un autre titre", "contents": "un autre contenu"}
    # عرض الصفحة
    return make_response(render_template("views/exemple_03.html", page=page))


# الصفحة الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

الرمز مشابه لرمز [exemple_02.py]. في السطر 16، نوضح كيف يمكن الإشارة إلى المستندات الموجودة في المجلدات الفرعية لـ [template_folder] الوارد في السطر 7.

يؤدي تشغيل البرنامج النصي [exemple_03.py] إلى ظهور النتائج التالية في المتصفح:

Image

22.3. البرنامج النصي [flask/02]: خدمة الويب الخاصة بالتاريخ والوقت

Image

المستند [date_time_server.html] هو التالي:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
  • السطر 8: تدعم الصفحة المعلمة [page.date_heure]؛

خدمة الويب [date_time_server.py] هي كما يلي:


# الاستيرادات
import os
import time

from flask import Flask, make_response, render_template

# تطبيق Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}")


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # إرسال الوقت إلى العميل
    # time.localtime: عدد الميلي ثانية منذ 01/01/1970
    # time.strftime يسمح بتنسيق الوقت والتاريخ
    # تنسيق عرض التاريخ والوقت
    # d: اليوم برقمين
    # m: الشهر برقمين
    # y: السنة برقمين
    # H: الساعة 0,23
    # M: الدقائق
    # S: الثواني

    # التاريخ/الساعة في الوقت الحالي
    time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
    # يتم إنشاء المستند المراد إرساله إلى العميل
    page = {"date_heure": time_of_day}
    document = render_template("date_time_server.html", page=page)
    print("document", type(document), document)
    # الرد HTTP إلى العميل
    response = make_response(document)
    print("response", type(response), response)
    return response


# يدويًا فقط
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 13: لا تقدم تطبيق الويب سوى URL
  • الأسطر 15-24: تشرح كيفية الحصول على التاريخ والوقت وكيفية عرضهما؛
  • السطر 27: سلسلة أحرف تمثل التاريخ والوقت الحاليين؛
  • الأسطر 28-30: يتم إنشاء المستند الديناميكي [date_time_server.html] عن طريق تمرير القاموس [page] الموجود في السطر 29 إليه؛
  • السطر 31: يتم عرض نوع [document] والمستند نفسه. نريد إظهار أنه سلسلة أحرف؛
  • السطر 33: يتم إنشاء الاستجابة HTTP التي سيتم إرسالها إلى العميل (لم يتم إرسالها بعد)؛
  • السطر 34: يتم عرض نوعها وقيمتها؛
  • السطر 35: يتم إرسال الاستجابة HTTP إلى العميل؛

يؤدي تنفيذ البرنامج النصي إلى ظهور النتيجة التالية في المتصفح:

Image

سجلات وحدة التحكم هي كما يلي:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\flask\02\date_time_server.py
 * Serving Flask app "date_time_server" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (اضغط على CTRL+C للخروج)
127.0.0.1 - - [10/Jul/2020 09:32:09] "GET / HTTP/1.1" 200 -
document <class 'str'> <!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : 10/07/20 09:42:33</b>
</body>
</html>
response <class 'flask.wrappers.Response'> <Response 195 bytes [200 OK]>
  • السطر 10: نلاحظ أن نوع القيمة التي أعادتها [render_template] هو من النوع [str]. هذه السلسلة من الأحرف ليست سوى المستند [date_time_server.html] بعد تفسيره (الأسطر 10-19)؛
  • السطر 20: نلاحظ أن نوع القيمة التي تُرجعها الدالة [make_response] هو [flask.wrappers.Response]. وقد تم استدعاء الدالة [Response.__str__] ضمناً لعرض الكائن [Response]. تقدم السلسلة التي تُرجعها هذه الدالة معلومتين عن الاستجابة HTTP التي سيتم إجراؤها:
    • يبلغ حجم المستند المرسل 195 بايت؛
    • حالة الاستجابة HTTP هي [200 OK]. سنرى لاحقًا أنه يمكننا الوصول إلى رمز الحالة هذا؛

22.4. نصوص برمجية [flask/03]: خدمات الويب التي تولد نصًا عاديًا

لقد رأينا في مثال سابق أن خدمة الويب تقدم المستند التالي:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>

قد لا يهتم عميل الويب إلا بالمعلومات [page.date_heure] الموجودة في السطر 8، وليس بالتنسيق HTML المحيط بها. ويمكن لخدمة الويب تقديم هذه المعلومات كسلسلة أحرف بسيطة. وسنقدم هنا أمثلة على هذا النوع من خدمات الويب.

22.4.1. نص برمجي [main_01]

Image

  • [main_01] هي خدمة الويب؛
  • [config] هو البرنامج النصي لتكوين تطبيق الويب؛
  • تستخدم خدمة الويب بعض الكيانات المحددة في [2]؛

النص البرمجي [config] هو التالي:


def configure():
    # المسار المطلق الذي يشير إلى المسارات النسبية للتكوين
    rootDir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # تبعيات التطبيق
    absolute_dependencies = [
        # الأشخاص، الأدوات، MyException
        f"{rootDir}/classes/02/entities",

    ]
    # يتم تعيين مسار النظام
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # تفعيل التكوين
    return {}

تتمثل المهمة الأساسية لهذا التكوين في تحديد مسار Python لخدمة الويب. يجب أن نتمكن من العثور على الكيانات [2] (السطر 8).

النص البرمجي للويب [main_01] هو كما يلي:


# يتم تكوين التطبيق
import config
config=config.configure()

# الاستيرادات
from flask import Flask, make_response
from flask_api import status

# التبعيات
from Personne import Personne

# تطبيق Flask (لا توجد مستندات ثابتة هنا)
app = Flask(__name__)


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # شخص
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    # الرد HTTP
    response = make_response(str(personne))
    # رؤوس HTTP
    response.headers.set("Content-type", "application/json; charser=utf8")
    # يتم إرجاع الرد HTTP
    return response, status.HTTP_200_OK


# البرنامج الرئيسي فقط
if __name__ == '__main__':
    # يتم تشغيل الخادم
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • الأسطر 1-3: يتم تحديد مسار Python للتطبيق؛
  • الأسطر 5-10: يتم استيراد العناصر التي يحتاجها البرنامج النصي؛
  • السطر 17: لا تقدم خدمة الويب سوى URL
  • السطر 20: يتم إنشاء كائن [Personne
  • السطر 22: يتم إنشاء استجابة HTTP باستخدام سلسلة الأحرف التي تمثل الشخص. سيتم استدعاء الدالة [Personne.__str__]. وتقوم هذه الدالة بإرجاع السلسلة jSON من قاموس [asdict] الخاص بالشخص (انظر |فئة BaseEntity|). معلمة الدالة [make_response] هي المستند النصي المرسل إلى العميل، أي في هذه الحالة السلسلة jSON الخاصة بشخص ما؛
  • السطر 24: نضع في رؤوس HTTP للرد، رأسًا [Content-type] الذي يُشير للعميل إلى نوع المستند الذي سيتلقاه، وهو هنا مستند jSON مُشفَّر بـ UTF-8؛
  • السطر 26: يتم إرجاع مجموعة مكونة من عنصرين:
    • الاستجابة للعميل، ورؤوس HTTP والمستند؛
    • رمز حالة الاستجابة. نريد هنا عرض رمز الحالة [200 OK]. يتم تعريف رموز الحالة المختلفة بواسطة ثوابت في الوحدة النمطية [flask_api] التي تم استيرادها في السطر 7؛

الوحدة النمطية [flask_api] غير متوفرة بشكل افتراضي. لذا يجب تثبيتها. يتم ذلك في محطة طرفية PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask_api
Collecting flask_api
  Downloading Flask_API-2.0-py3-none-any.whl (119 kB)
     || 119 kB 544 kB/s
Requirement already satisfied: Flask>=1.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_api) (1.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.0.1)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (7.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.1.0)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask>=1.1->flask_api) (1.1.1
)
Installing collected packages: flask-api
Successfully installed flask-api-2.0

عند تشغيل البرنامج النصي للويب [main_01]، نحصل على النتائج التالية في المتصفح:

Image

  • في [2]، السلسلة jSON المستلمة؛
  • في [3-4]، يتم عرض محتوى المستند المستلم. نلاحظ أنه لا يوجد أي تضمين HTML، بل السلسلة jSON فقط؛

لنلقِ نظرة الآن على دور الرأس [Content-Type] الذي أرسلته خدمة الويب إلى العميل. نضع المتصفح في وضع المطور (F12 بشكل عام) ونطلب مرة أخرى نفس URL. فيما يلي لقطة شاشة لمتصفح Chrome:

Image

  • في [1]، حدد علامة التبويب [Network]؛
  • في [2, 4]: URL المطلوب من قبل المتصفح؛
  • في [3]، حدد علامة التبويب [Headers] (رؤوس HTTP
  • في [5]، رمز حالة الاستجابة HTTP المستلمة؛
  • في [6]، الرأس الذي يُعلم العميل بأنه سيتلقى نصًا jSON. وهذا يسمح للعميل بالتكيف مع الرد. وبالتالي، فإن خط الحروف الذي يستخدمه متصفح Chrome لعرض استجابة jSON أو استجابة نصية أساسية ليس هو نفسه؛

Image

  • في [8]، نختار علامة التبويب [Response] للوصول إلى المستند المرسل من خدمة الويب، وهو هنا سلسلة بسيطة jSON؛

22.4.2. Postman

[Postman] هي الأداة التي ستسمح لنا باستعلام مختلف URL لتطبيق ويب. وهي تتيح لنا:

  • استخدام أي URL: حيث يتم إنشاء هذه الرموز يدويًّا؛
  • إرسال استعلامات إلى خادم الويب باستخدام ملفات GET، POST، PUT، OPTIONS…؛
  • تحديد معلمات GET أو POST؛
  • تحديد رؤوس HTTP للطلب؛
  • تلقي استجابة بتنسيق jSON، XML، HTML،
  • الوصول إلى رؤوس الاستجابة HTTP. وبذلك يمكننا الوصول إلى الاستجابة الكاملة HTTP من الخادم؛

يُعد [Postman] أداة تعليمية ممتازة لفهم الاتصال بين العميل والخادم في بروتوكول HTTP.

[Postman] متاح على URL [https://www.getpostman.com/downloads/]. قم بتثبيت إصدارك من [Postman]. أثناء التثبيت، سيُطلب منك إنشاء حساب: لن يكون لهذا الحساب أي فائدة هنا. يُستخدم حساب [Postman] لمزامنة الأجهزة المختلفة بحيث يتم نسخ إعدادات جهاز ما إلى جهاز آخر. لا فائدة من أي من هذا هنا.

بمجرد التثبيت، يعرض [Postman] الواجهة التالية:

Image

  • في [2-3]، يمكن الوصول إلى إعدادات المنتج؛

Image

  • في [6]، الإصدار المستخدم في هذا المستند؛

سنستخدم هنا [Postman] لاختبار خدمة الويب jSON السابقة:

  • نقوم بتنفيذ البرنامج النصي [flask/03/main_01]؛
  • ثم نطلب URL [http://localhost:5000/] باستخدام Postman؛ Image
  • في [1]، نقوم بإنشاء طلب؛
  • في [2]، سيكون الطلب هو HTTP و GET؛
  • في [3]، يتم استدعاء URL من خدمة الويب؛
  • في [4]، يتم إرسال الطلب إلى خدمة الويب؛ Image
  • في [5]، يتم تحديد علامة التبويب [Body] التي تعرض المستند المستلم؛
  • في [6]، يتم تحديد علامة التبويب [Pretty] التي تعرض المستند المستلم بتنسيق مناسب، وهو هنا تنسيق مناسب لسلسلة jSON؛
  • في [7]، المستند jSON المستلم؛
  • في [8-9]، المستند المستلم بدون تنسيق؛ Image
  • في [10]، يتم عرض الرؤوس HTTP التي تم استلامها عبر Postman؛
  • في [11]، حالة HTTP للاستجابة المستلمة؛
  • في [12]، الرؤوس HTTP التي تم استلامها؛
  • في [13]، الرأس [Content-type] الذي سمح لـ Postman بمعرفة أنه سيتلقى سلسلة jSON. استخدم Postman هذه المعلومات لتنسيق المستند المستلم بطريقة معينة؛

هناك طريقة أخرى لاستخدام Postman. وهي تتمثل في استخدام وحدة التحكم في Postman (Ctrl-Alt-C). تتيح هذه الوحدة الاطلاع على الحوار بين العميل والخادم. وبالإضافة إلى تسلسل المفاتيح Ctrl-Alt-C، تتوفر وحدة التحكم في Postman عبر أيقونة موجودة في أسفل يسار النافذة الرئيسية لـ Postman:

Image

تقوم وحدة التحكم في Postman بتسجيل الحوارات بين العميل والخادم التي تحدث عند تنفيذ طلب Postman:

Image

  • في [3]، قائمة الطلبات التي أرسلها Postman منذ تشغيله. تظهر أحدث الطلبات في أسفل القائمة؛
  • في [4]، الطلب HTTP الذي أرسله Postman؛
  • في [5-6]، الرد HTTP الذي قدمه خادم الويب؛
  • في [7]، يمكن رؤية السجلات في الوضع [raw]، أي بدون أي تعديلات في العرض؛

في الوضع [raw]، تصبح نافذة وحدة التحكم كما يلي:

Image

  • في [8]، الطلب HTTP الذي أرسله Postman إلى خادم الويب؛
  • في [9]، الرد HTTP الذي أرسله خادم الويب؛
  • في [10]، يمكن العودة إلى الوضع [pretty logs]؛

لتسهيل الشرح، سنقوم بترقيم الأسطر التي تم الحصول عليها من وحدة التحكم Postman.

بالنسبة للعميل:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 70e2acaa-b3e5-46f6-8375-989e6b94e694
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

بالنسبة للخادم:

1
2
3
4
5
6
HTTP/1.0 200 OK
Content-type: application/json; charser=utf8
Content-Length: 56
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:19:56 GMT
{"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87}

من الآن فصاعدًا، سنستخدم بشكل أساسي:

  • [Postman] كعميل ويب؛
  • وحدة التحكم [Postman] في [raw mode] لشرح الحوار بين العميل والخادم؛

22.4.3. النص البرمجي [main_02]

Image

النص البرمجي للويب [main_02] هو كما يلي:


# يتم تكوين التطبيق
import config
config=config.configure()

# عمليات الاستيراد
from flask import Flask, make_response
from flask_api import status

# التبعيات
from Personne import Personne

# تطبيق Flask
app = Flask(__name__)


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # شخص
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    # المحتوى
    response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
    # رؤوس HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    # الرد HTTP
    return response, status.HTTP_200_OK


# اليد فقط
if __name__ == '__main__':
    # يتم تشغيل الخادم
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • البرنامج النصي [main_02] مشابه للبرنامج النصي [main_01]. ويختلف عنه في نقطتين:
    • السطر 22: المستند المرسل إلى العميل هو سلسلة أحرف خام، وليس سلسلة jSON؛
    • السطر 24: ينعكس هذا في رأس HTTP [Content-Type] الذي يشير إلى نوع [text/plain] للمستند؛

نقوم بتنفيذ البرنامج النصي للويب [main_02] ثم نستخدم [Postman] للاستعلام عنه:

Image

  • في [1-3]، يتم إرسال الطلب إلى خدمة الويب؛
  • في [5]، الحالة OK للاستجابة؛
  • في [4, 6]، رؤوس الاستجابة HTTP؛
  • في [7]، الرأس [Content-Type]؛
  • إلى [8-10]، المستند المرسل من خدمة الويب، وهو سلسلة أحرف؛

تُظهر وحدة التحكم Postman السجلات التالية:

طلب العميل:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 7c7fc9f3-8df8-49ae-9dc8-53c2d87d111a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

استجابة الخادم:


HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf8
Content-Length: 34
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:34:22 GMT

personne[Aglaë, de la Hûche, 87]

22.4.4. نص برمجي [main_03]

Image

نص البرنامج النصي للويب [main_03] هو كما يلي:


# يتم تكوين التطبيق
import config
config = config.configure()

# عمليات الاستيراد
from flask import Flask, make_response
from flask_api import status

# التبعيات
from MyException import MyException
from Personne import Personne

# تطبيق Flask
app = Flask(__name__)


# الصفحة الرئيسية URL
@app.route('/')
def index():
    # شخص غير صحيح
    msg_erreur = None
    try:
        personne = Personne().fromdict({"prénom": "", "nom": "", "âge": 87})
    except MyException as erreur:
        msg_erreur = f"{erreur}"
    # خطأ؟
    if msg_erreur:
        response = make_response(msg_erreur)
        status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    else:
        response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
        status_code = status.HTTP_200_OK
    # رؤوس HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    # الرد HTTP
    return response, status_code


# اليد فقط
if __name__ == '__main__':
    # يتم تشغيل الخادم
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 23: يحدث خطأ عند إنشاء مثيل لشخص غير صحيح؛
  • الأسطر 27-29: بسبب الخطأ:
    • السطر 28: يتم إعداد استجابة HTTP تحتوي على رسالة الخطأ؛
    • السطر 29: يتم تعيين قيمة الخطأ [500 Internal Server Error] لرمز الحالة HTTP؛
  • السطر 34: يتم إخطار العميل بأنه سيتم إرسال نص عادي إليه؛
  • السطر 36: يتم إرسال الرد HTTP إلى العميل؛

نقوم بتشغيل خدمة الويب [main_03] ونستخدم Postman لاستعلامها:

Image

  • في [1-3]، نرسل الطلب؛
  • في [4]، نحصل على رد برمز الحالة [500 INTERNAL SERVER ERROR
  • في [5-7]: الرد عبارة عن نص يصف الخطأ الذي حدث؛

Image

  • في [8-10]، تظهر رؤوس HTTP الخاصة برد خدمة الويب؛

في وحدة التحكم Postman، تكون النتائج في الوضع [raw] كما يلي:

طلب العميل:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 925ff036-a360-47af-adf6-78173c01a247
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

استجابة الخادم:


HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Type: text/plain; charset=utf8
Content-Length: 74
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:39:24 GMT

MyException[11, Le prénom doit être une chaîne de caractères non vide]

22.5. نصوص [flask/04]: المعلومات المضمنة في الطلب

Image

يهدف البرنامج النصي [request_parameters.py] إلى إظهار أن خدمة الويب لديها إمكانية الوصول إلى معلومات متنوعة مضمنة في طلب عميل الويب. وفيما يلي الكود:


# استيراد
from flask import Flask, make_response, request
from flask_api import status
# تطبيق Flask
app = Flask(__name__)


# الصفحة الرئيسية URL
@app.route('/', methods=['GET', 'POST'])
def index():
    # معلمات الطلب
    request_data = {}
    request_data["environ"] = f"{request.environ}"
    request_data["path"] = request.path
    request_data["full_path"] = request.full_path
    request_data["script_root"] = request.script_root
    request_data["url"] = request.url
    request_data["base_url"] = request.base_url
    request_data["url_root"] = request.url_root
    request_data["accept_charsets"] = request.accept_charsets
    request_data["accept_encodings"] = request.accept_encodings
    request_data["accept_languages"] = request.accept_languages
    request_data["accept_mimetypes"] = request.accept_mimetypes
    request_data["args"] = request.args
    request_data["content_encoding"] = request.content_encoding
    request_data["content_length"] = request.content_length
    request_data["content_type"] = request.content_type
    request_data["endpoint"] = request.endpoint
    request_data["files"] = request.files
    request_data["form"] = request.form
    request_data["host"] = request.host
    request_data["method"] = request.method
    request_data["query_string"] = request.query_string.decode()
    request_data["referrer"] = request.referrer
    request_data["remote_addr"] = request.remote_addr
    request_data["remote_user"] = request.remote_user
    request_data["scheme"] = request.scheme
    request_data["script_root"] = request.script_root
    request_data["user_agent"] = f"{request.user_agent}"
    request_data["values"] = request.values
    # الاستجابة HTTP
    response = make_response(request_data)
    # رؤوس HTTP
    response.headers["Content-Type"] = "application/json; charset=utf-8"
    # إرسال الرد HTTP
    return response, status.HTTP_200_OK


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 9: نجري تغييرًا. نحدد الأفعال المسموح بها في طلب العميل. يقدم Postman قائمة بها:

Image

أول اثنين، وهما [GET, POST]، هما الأكثر استخدامًا وسيكونان أيضًا الوحيدين المستخدمين في هذا المستند. بالعودة إلى السطر 9 من الكود، يحتوي المعلمة [methods] على قائمة بالطرق المذكورة أعلاه والمسموح بها من قبل URL. وفي حالة عدم وجود هذا المعامل، تُسمح فقط بالطريقة [GET]. وهذا ما كان يحدث حتى الآن؛

  • السطر 12: سنقوم بإنشاء القاموس [request_data]؛
  • السطر 13: طلب العميل متاح في كائن مُعرَّف مسبقًا [request]، تم استيراده في السطر 2، من النوع [werkzeug.local.LocalProxy]. تسترد الأسطر التالية سمات متنوعة من هذا الكائن؛
  • بدلاً من تفصيل كل سمة من سمات الكائن [request]، سنقوم بتنفيذ هذا الكود ونلقي نظرة على النتائج. عندها سنفهم بشكل أفضل معنى السمات المختلفة المعروضة؛
  • السطر 42: سيكون القاموس [request_data] هو محتوى الاستجابة HTTP. نتذكر أن هذا المحتوى يجب أن يكون نصًا. يقوم Flask تلقائيًا بتحويل القواميس إلى سلاسل نصية jSON؛
  • السطر 44: يتم إخطار العميل بأنه سيتلقى jSON؛
  • السطر 46: يتم إرسال الرد إلى العميل؛

باستخدام عميل Postman، نرسل الطلب التالي إلى خدمة الويب السابقة:

Image

  • في [1-2]، الطلب المرسل؛
  • في [2]، يتم تعيين معلمات الطلب. يتم إرفاق المعلمات بـ URL في شكل [ ?param1=valeur1&param2=valeur2]. هناك طريقتان لإدخال هذه المعلمات في Postman:
    • كتابتها مباشرةً في URL؛
    • كتابتها في [3-4]؛

الطريقتان متكافئتان؛

نضيف معلمات أخرى إلى الطلب:

Image

  • في [5-7]، نضيف معلمات في نص الطلب (=body). في حين أن معلمات URL مرئية لمستخدم متصفح الويب، فإن تلك التي تشكل جزءًا من نص الطلب غير مرئية. يقوم المتصفح (أو Postman في هذه الحالة) بإرسالها إلى الخادم بعد رؤوس HTTP. وبذلك يصبح لطلب العميل على الويب نفس بنية استجابة خادم الويب: رؤوس HTTP تليها وثيقة. سيؤدي ذلك إلى ظهور رأسين جديدين HTTP في طلب العميل:
    • [Content-Type]: يُخبر العميل الخادم بنوع المستند الذي يرسله؛
    • [Content-Length]: حجم المستند بالبايت؛
  • في [6]، الترميز الذي يجب استخدامه للمعلمات المعلنة في [7]. ويمكن ترميز هذه المعلمات بطرق متنوعة. [x-www-form-urlencoded] هي طريقة تستخدمها المتصفحات بشكل متكرر؛

يمكننا رؤية الطلب الذي سيتم إنشاؤه:

Image

الاستجابة لهذا الطلب هي كما يلي:

Image

  • في [1-5]، تلقينا سلسلة jSON [3]؛
  • ما يهم خدمة الويب عادةً هو معلمات URL و[ ?param1=valeur1&param2=valeur2] وتلك التي تم إرسالها في نص الطلب (المستند). هكذا، بشكل عام، يقوم العميل بإرسال المعلومات إليها. ونلاحظ في [5] أن معلمات URL متوفرة في [request.args]؛

أما باقي الاستجابة فهو كما يلي:

Image

  • في [9]، سمات المعلمات الموضوعة في نص الطلب:
    • [content_type] هو نوع المستند المرفق بالطلب. وقد رأينا أن هذا المستند يحتوي على معلومات من النوع [param=valeur] مشفرة في شكل [x-www-form-urlencoded]. لذا، قام Postman بإنشاء رأس HTTP [Content-Type] يشير إلى طبيعة المستند؛
    • [content_length] هو حجم هذا المستند بالبايت؛
  • في [10]، تحتوي السمة [request.environ] على العديد من المعلومات حول البيئة التي تتم فيها معالجة طلب العميل. وتوجد معظم هذه المعلومات في السمات الأخرى للكائن [request]؛
  • في [11]، تتوفر المعلمات الموجودة في نص الطلب في السمة [request.form]؛
  • في [12]، الطريقة المستخدمة لإرسال الطلب، وهي في هذه الحالة الطريقة [GET
  • في [13]، السمة [request.values] هي قاموس جميع المعلمات، سواء تلك الموجودة في URL أو تلك الموجودة في نص المستند. للحصول على معلمات الطلب، سنستخدم السمة:
    • [request.args] للحصول على المعلمات الموجودة في URL؛
    • [request.form] للحصول على المعلمات الموجودة في نص المستند؛

في وحدة التحكم Postman، تكون السجلات كما يلي:

طلب العميل:

GET /?param1=valeur1&param2=valeur2 HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: cbfac6aa-71a0-4076-a0c3-91d36d74a4c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 60

nom=s%C3%A9l%C3%A9n%C3%A9&pr%C3%A9nom=agla%C3%AB&%C3%A2ge=77
  • السطر 9: نوع المستند المرسل في السطر 12 إلى الخادم؛
  • السطر 11: يتم فصل رؤوس الطلب HTTP عن المستند المرسل بسطر فارغ. وبهذه الطريقة يتعرف الخادم على نهاية رؤوس الطلب HTTP المرسلة من العميل؛
  • السطر 12: المستند «المشفّر بـ url». خضعت جميع الأحرف المُشَدَّدة للتشفير؛

رد العميل هو كما يلي:


HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 2433
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:09:09 GMT

{
  "accept_charsets": [], 
  "accept_encodings": [
    [
      "gzip", 
      1
    ], 
    [
      "deflate", 
      1
    ], 
    [
      "br", 
      1
    ]
  ], 
  "accept_languages": [], 
  "accept_mimetypes": [
    [
      "*/*", 
      1
    ]
  ], 
  "args": {
    "param1": "valeur1", 
    "param2": "valeur2"
  }, 
  "base_url": "http://localhost:5000/", 
  "content_encoding": null, 
  "content_length": 60, 
  "content_type": "application/x-www-form-urlencoded", 
  "endpoint": "index", 
  "environ": "{'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': <_io.BufferedReader name=908>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x00000173CA6E5160>, 'SERVER_SOFTWARE': 'Werkzeug/1.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': '/', 'QUERY_STRING': 'param1=valeur1&param2=valeur2', 'REQUEST_URI': '/?param1=valeur1&param2=valeur2', 'RAW_URI': '/?param1=valeur1&param2=valeur2', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': 50592, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_USER_AGENT': 'PostmanRuntime/7.26.1', 'HTTP_ACCEPT': '*/*', 'HTTP_CACHE_CONTROL': 'no-cache', 'HTTP_POSTMAN_TOKEN': 'cbfac6aa-71a0-4076-a0c3-91d36d74a4c0', 'HTTP_HOST': 'localhost:5000', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_CONNECTION': 'keep-alive', 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': '60', 'werkzeug.request': <Request 'http://localhost:5000/?param1=valeur1&param2=valeur2' [GET]>}", 
  "files": {}, 
  "form": {
    "nom": "s\u00e9l\u00e9n\u00e9", 
    "pr\u00e9nom": "agla\u00eb", 
    "\u00e2ge": "77"
  }, 
  "full_path": "/?param1=valeur1&param2=valeur2", 
  "host": "localhost:5000", 
  "method": "GET", 
  "path": "/", 
  "query_string": "param1=valeur1&param2=valeur2", 
  "referrer": null, 
  "remote_addr": "127.0.0.1", 
  "remote_user": null, 
  "scheme": "http", 
  "script_root": "", 
  "url": "http://localhost:5000/?param1=valeur1&param2=valeur2", 
  "url_root": "http://localhost:5000/", 
  "user_agent": "PostmanRuntime/7.26.1", 
  "values": {
    "nom": "s\u00e9l\u00e9n\u00e9", 
    "param1": "valeur1", 
    "param2": "valeur2", 
    "pr\u00e9nom": "agla\u00eb", 
    "\u00e2ge": "77"
  }
}
  • الأسطر 1-5: رؤوس الاستجابة HTTP تنتهي بسطر فارغ؛
  • الأسطر 41-45: خضعت الأحرف التي تحتوي على علامات التشكيل لعملية الترميز UTF-8؛

وإذا استخدمنا الآن الطريقة [POST] لإرسال نفس الطلب بنفس المعلمات، فسنحصل على نفس الرد، باستثناء أنه في الطريقة [12]، سنحصل على [‘method’ : ‘POST’].

فما الفرق بين الطريقتين GET و POST؟ الفرق طفيف وقد نشأ عن الاستخدام التاريخي لهذه الطرق من قِبل المتصفحات:

  • تعد المعلمات في URL عملية لأن ملف URL الذي تم تهيئته بهذه الطريقة يمكن أن يُستخدم كرابط في مستند HTML. يمكن للمستخدم أيضًا تغيير المعلمات بنفسه للحصول على استجابات مختلفة من الخادم. في هذه الحالة، تستخدم المتصفحات عادةً طريقة [GET] ولا يوجد نص (content_length=0) في الطلب المرسل إلى خادم الويب (لا توجد معلمات مخفية)؛
  • أحيانًا لا نرغب في عرض المعلمات في URL. وهذا هو الحال بالنسبة لكلمات المرور المرسلة إلى الخادم. علاوة على ذلك، فإن الحجم الذي تشغله معلمات URL محدود (لا يمكن أن يتجاوز حجم URL حدًا معينًا). أما معلمات نص الطلب فلا تخضع لهذا القيد. كما أن وجود عدد كبير من المعلمات في URL يجعلها غير قابلة للقراءة. لنأخذ المثال الشائع المتمثل في نموذج التسجيل في أحد مواقع الويب. تاريخيًا، عندما لم تكن صفحات HTML تحتوي بعد على جافا سكريبت، كانت المتصفحات ترسل المعلومات التي تم إدخالها عبر POST. وكان يُشار إلى ذلك آنذاك بالقيم المرسلة؛

لذلك في بدايات برمجة الويب:

  • كانت طرق GET ترتبط بالأحرى بطلب المعلومات المقدمة من خادم الويب؛
  • كانت طرق POST مرتبطة بشكل أكبر بإرسال المعلومات من المتصفح إلى الخادم. وكان الخادم آنذاك «يُثري» بمعلومات؛

ومنذ ذلك الحين، دخلت لغة جافا سكريبت (JavaScript) على الخط. ففي حين أن المطور لم يكن له دور في الأمثلة السابقة (كان النقر على رابط يؤدي حتمًا إلى تشغيل GET، وكان إرسال نموذج يتطلب بالضرورة استخدام POST)، فإن جافا سكريبت أعادت له زمام الأمور. في هذا النموذج، ترتبط الصفحة HTML برمز جافا سكريبت يمكنه تجاوز المتصفح. وبالتالي، يمكن لرمز جافا سكريبت اعتراض النقر على رابط ما، ليقوم بعد ذلك بتنفيذ رمز يرسل طلبًا إلى الخادم. وسيكون هذا الطلب غير مرئي للمستخدم. لن يراه المستخدم. هذا الكود هو عميل ويب، وكما فعلنا مع Postman، يمكن للمطور إنشاء الطلب الذي يريده. للعودة إلى النقر على الرابط، يمكنه تنفيذ POST، في حين أن المتصفح كان سيقوم افتراضيًا بتنفيذ GET. وقد أدت هذه التطورات إلى تقليل أهمية الفروق بين GET و POST.

ومع ذلك، غالبًا ما يتبع المطورون القواعد التالية:

  • يجب ألا يغير GET حالة الخادم. ويجب أن تُرجع عمليات GET المتتالية التي تُجرى باستخدام نفس المعلمات الموجودة في URL نفس المستند. بالإضافة إلى ذلك، غالبًا ما لا يحتوي GET على نص (لا يوجد مستند مرتبط به)، بل يحتوي فقط على معلمات في URL؛
  • يمكن لـ POST تغيير حالة الخادم. غالبًا ما يتم إرسال المعلمات في نص الطلب. ويُشار إلى ذلك باسم «القيم المرسلة». ويعد مثال النموذج الأكثر وضوحًا: حيث تُدرج القيم التي يدخلها المستخدم في نص طلب POST ويقوم الخادم بتسجيلها في مكان ما، غالبًا في قاعدة بيانات؛

في بقية هذا المستند، لن نلتزم بأي قاعدة معينة.

22.6. نصوص [flask-05]: إدارة ذاكرة المستخدم

22.6.1. مقدمة

في الأمثلة السابقة للعميل/الخادم، كان العمل يسير على النحو التالي:

  • يقوم العميل بفتح اتصال بالمنفذ 80 لجهاز خدمة الويب؛
  • يرسل تسلسل النص: الرؤوس HTTP، سطر فارغ، [document
  • رداً على ذلك، يرسل الخادم تسلسلاً من نفس النوع؛
  • يقوم الخادم بإنهاء الاتصال مع العميل؛
  • يقوم العميل بإنهاء الاتصال بالخادم؛

إذا أرسل نفس العميل طلبًا جديدًا إلى خادم الويب بعد ذلك بوقت قصير، يتم إنشاء اتصال جديد بين العميل والخادم. ولا يمكن للخادم معرفة ما إذا كان العميل الذي يتصل قد زاره من قبل أم أن هذا هو الطلب الأول. بين اتصالين، «ينسى» الخادم عميله. ولهذا السبب، يُقال إن بروتوكول HTTP هو بروتوكول عديم الحالة. ومع ذلك، من المفيد أن يتذكر الخادم عملاءه. فعلى سبيل المثال، إذا كان التطبيق آمنًا، سيرسل العميل إلى الخادم اسم المستخدم وكلمة المرور لتوثيق هويته. وإذا «نسي» الخادم عميله بين اتصالين، فسيضطر العميل إلى توثيق هويته عند كل اتصال جديد، وهو أمر غير مقبول.

ولمتابعة العميل، يمكن للخادم اتباع طرق مختلفة:

  1. عند تلقي طلب أول من عميل، يضمّن الخادم في رده معرّفًا يجب على العميل إرساله مرة أخرى في كل طلب جديد. وبفضل هذا المعرّف، الذي يختلف من عميل لآخر، يمكن للخادم التعرف على العميل. ويمكنه عندئذ إدارة ذاكرة لهذا العميل في شكل ذاكرة مرتبطة بشكل فريد بمعرّف العميل. وهكذا تعمل، على سبيل المثال، خدمات PHP؛
  2. عند الطلب الأول من العميل، لا يضمّن الخادم في رده معرّفًا، بل ذاكرة المستخدم نفسها. ولا يحتفظ الخادم بأي شيء على جانبه. وللحفاظ على ذاكرته، يجب على عميل الويب إعادة إرسال هذه الذاكرة مع كل طلب جديد. ويتم تعديل هذه الذاكرة (أو لا) مع كل طلب جديد وإعادة إرسالها (أو لا) إلى العميل. وهذه هي الطريقة التي يستخدمها إطار العمل Flask؛

والاختلافات بين الطريقتين هي كما يلي:

  • الطريقة 1 تستهلك نطاقًا تردديًّا أقل. لا يتم تبادل سوى معرّف واحد بين العميل والخادم. وعندما تزداد ذاكرة المستخدم، لا يؤثر ذلك على المعرّف الذي يظل كما هو. وهذا ليس هو الحال في الطريقة 2 حيث يتم تبادل ذاكرة المستخدم مع كل طلب ويمكن أن تزداد حجمًا مع تراكم الطلبات؛
  • الطريقة 1 تستهلك مساحة أكبر من الذاكرة. ففي الواقع، يقوم الخادم بتخزين ذاكرة المستخدم على أنظمة الملفات الخاصة به. وإذا كان هناك مليون مستخدم، فقد يشكل ذلك مشكلة. أما الطريقة 2 فلا تخزن أي شيء على الخادم؛

من الناحية التقنية، تسير الأمور على النحو التالي في كلتا الطريقتين:

  • في الرد على عميل جديد، يضمّن الخادم الرأس HTTP أو [Set-Cookie : MotClé=Identifiant] أو [Set-Cookie : mémoire]. في الطريقة 1، لا يقوم الخادم بذلك إلا عند الطلب الأول. أما في الطريقة 2، فيقوم بذلك في كل مرة تتغير فيها ذاكرة المستخدم؛
  • في طلباته، يقوم العميل بإعادة إرسال ما تلقّاه بشكل منهجي، سواء كان معرّفًا أو ذاكرة. ويقوم بذلك عبر الرأس HTTP [Cookie : MotClé=Valeur

قد يتساءل المرء كيف يعرف الخادم أنه يتعامل مع عميل جديد بدلاً من عميل سبق له الزيارة. ويتم تحديد ذلك من خلال وجود الرأس HTTP Cookie ضمن رؤوس HTTP الخاصة بالعميل. أما بالنسبة للعميل الجديد، فإن هذا الرأس يكون غائبًا.

يُطلق على مجموعة الاتصالات الخاصة بعميل معين اسم «جلسة».

يمكن للخادم الاحتفاظ بأنواع أخرى من الذاكرة:

Image

  • في [1]، تتميز ذاكرة الطلب بطابع خاص. فهي تُستخدم عندما لا تتم معالجة طلب العميل عبر الويب بواسطة خدمة (أو تطبيق) واحدة، بل بواسطة عدة خدمات. ولنقل المعلومات إلى الخدمة i+1، يمكن للخدمة i أن تضيف هذه المعلومات إلى الطلب الذي تمت معالجته (request). وهذا ما يُسمى بـ«ذاكرة مستوى الطلب». لن نستخدم هذا النوع من الذاكرة في هذا المستند؛
  • في [2, 4]، ذاكرة المستخدم التي وصفناها للتو. يمكن تنفيذها محليًّا [2] أو صيانتها باستخدام العميل [4]؛
  • في [3]، تكون ذاكرة مستوى «التطبيق» عادةً ذاكرة للقراءة فقط. وهي مشتركة بين جميع المستخدمين. وغالبًا ما تحتوي على عناصر من تكوين تطبيق الويب، وهو التكوين المشترك بين جميع مستخدمي التطبيق. يجب توخي الحذر عند التعامل مع هذا النوع من الذاكرة: يجب أن تتم الكتابة فيها في وقت لم يرسل فيه المستخدمون أي طلبات بعد، وغالبًا ما يكون ذلك عند بدء تشغيل التطبيق. وبعد ذلك، عند وصول الطلبات، يصعب الكتابة في هذه الذاكرة. فعندما يخدم خادم الويب عدة مستخدمين في وقت واحد ويرغب اثنان منهم في الكتابة في ذاكرة مستوى «التطبيق»، يكون هناك خطر من تلف هذه الذاكرة. ففي الواقع، عندما يبدأ المستخدم 1 في الكتابة في ذاكرة مستوى «التطبيق»، قد يتم مقاطعته قبل أن ينتهي حتى. ونحصل عندئذٍ على ذاكرة تطبيق غير مكتملة. ونظرًا لأنها مشتركة، يمكن للمستخدم 2 قراءتها والحصول على حالة غير صحيحة؛

22.6.2. نص برمجي [session_scope_01]

Image

توضح البرامج النصية [session_scope_xx] إدارة ذاكرات المستخدمين.

النص البرمجي [session_scope_01] هو كما يلي:


# يتم تكوين التطبيق
import config
config = config.configure()

# التبعيات
import json
from flask import Flask, make_response, session
from flask_api import status

# تطبيق Flask
app = Flask(__name__)

# مفتاح الجلسة السري
app.secret_key = config["SECRET_KEY"]


@app.route('/set-session', methods=['GET'])
def set_session():
    # إضافة شيء ما إلى الجلسة
    session['nom'] = 'séléné'
    # إرسال استجابة فارغة
    response = make_response()
    response.headers['Content-Length'] = 0
    return response, status.HTTP_200_OK


@app.route('/get-session', methods=['GET'])
def get_session():
    # استرداد الجلسة وإرسال الرد
    response = make_response(json.dumps({"nom": session['nom']}, ensure_ascii=False))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# اليد فقط
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 11: يتم إنشاء مثيل لتطبيق Flask؛
  • السطر 14: يتم تعيين قيمة للسمة [secret_key] لهذا التطبيق، وهي قيمة مأخوذة من ملف التكوين المستخدم في الأسطر 1-3. لا يمكن إجراء جلسة عمل Flask إلا إذا تم تهيئة هذه السمة. يمكن إدخال أي قيمة فيها. وهي تُستخدم لتشفير جزء من «ذاكرة المستخدم» التي سيتم إرسالها إلى العميل. وعادةً ما يتم إدخال قيمة يصعب تخمينها. في الملف [config]، يتم تعريف المفتاح السري على النحو التالي:

    # نقوم بتعيين الإعدادات
    config = {
        # تكوين Flask
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }
  • لأول مرة، نُعرِّف تطبيق ويب يخدم غرضًا آخر غير URL /
    • السطر 17: يُستخدم URL [/set-session] لتهيئة جلسة عمل المستخدم؛
    • السطر 27: تُستخدم URL [/get-session] لاستعادة ذاكرة المستخدم (أو جلسة عمل المستخدم)؛
  • السطر 20: يتم إدخال شيء ما في ذاكرة المستخدم (= الجلسة)، وهو في هذه الحالة اسم. تُدار الجلسة بشكل يشبه إلى حد ما القاموس. لا يمكن إدخال أي شيء في الجلسة. يجب أن تكون القيم التي يتم إدخالها قابلة للتحويل إلى jSON. بالنسبة لأنواع Python المحددة مسبقًا، يتم ذلك دون تدخل من المطور. أما بالنسبة للكائنات المخصصة التي لا تعرفها Python، فيجب إجراء التحويل jSON بنفسك؛
  • السطر 22: يتم إنشاء استجابة HTTP خالية من المحتوى (عدم وجود معلمة في make_response
  • السطر 23: يتم إخطار العميل بأنه سيتلقى مستندًا فارغًا (بحجم 0 بايت)؛
  • السطر 24: يتم إرسال الرد HTTP إلى العميل. وبالتالي، فإن URL و[/set-session] لا يقومان بأي شيء سوى تهيئة جلسة عمل المستخدم؛
  • السطر 27: تسمح الردود URL و [/get-session] للمستخدم بمعرفة محتويات جلسته؛
  • السطر 30: يتم إنشاء استجابة HTTP تحتوي على السلسلة jSON الخاصة بجلسة عمل المستخدم. هنا قمنا بإنشاء السلسلة jSON بأنفسنا بدلاً من ترك Flask يقوم بإنشائها. في الواقع، لا نريد أن يتم تجاوز الأحرف المُشَدَّدة (ensure_ascii=False
  • السطر 31: نُعلم العميل بأننا نرسل له jSON؛
  • السطر 32: نرسل الرد HTTP إلى العميل؛

الغرض من هذا البرنامج النصي هو إظهار أن جلسة عمل المستخدم تسمح بربط طلباته المتتالية:

  • الطلب 1 سيطلب URL [/set-session]؛
  • الطلب 2 سيطلب URL و [/get-session] وسيسترد الاسم الذي سيكون الطلب 1 قد قام بتهيئته؛

النص البرمجي [config] الذي يقوم بتكوين النصوص البرمجية في المجلد [flask/05] هو كما يلي:


def configure():
    # المسار المطلق الذي يشير إلى المسارات النسبية للتكوين
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # تبعيات التطبيق
    absolute_dependencies = [
        # الأشخاص، الأدوات، MyException
        f"{root_dir}/classes/02/entities",
    ]
    # يتم تعيين مسار النظام
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # تفعيل التكوين
    config = {
        # تكوين Flask
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }

    return config

نقوم بتشغيل البرنامج النصي [session_scope_01]، ثم نستخدم Postman لطلب URL و[/set-session]. وقبل ذلك، سنتحقق من بعض عناصر الطلب الذي سيتم إجراؤه:

Image

  • في [1]، نصل إلى ملفات تعريف الارتباط في Postman؛ Image
  • في [2-4]، نتحقق من ملفات تعريف الارتباط المعروفة في Postman ونحذفها جميعًا [4-5]؛

الآن دعونا نتحقق من الطلب HTTP الذي سيتم إنشاؤه:

Image

  • في [9]: جزء من الرؤوس HTTP التي سيقوم Postman بإدراجها في الطلب بناءً على التكوين الذي قمنا بإجرائه له. يتيح لك هذا الفحص التأكد من أنك لم تغفل أي معلمات أو، على العكس، لم تترك معلمات غير ضرورية؛

وبعد ذلك، يمكن تنفيذ الاستعلام:

Image

هناك طرق مختلفة للتحقق من النتيجة. يمكننا أولاً إلقاء نظرة على النافذة الرئيسية:

Image

  • في [1-2]، الاستعلام الموجه إلى خدمة الويب؛
  • في [3-6]، رؤوس الاستجابة HTTP؛
  • في [4]، نظرًا لعدم تحديد نوع الرد في الكود، استخدم Flask النوع [text/html] افتراضيًا؛
  • في [5]، يعرف العميل أنه لا يوجد مستند في الرد؛
  • السطر 6: تم إرسال الرأس [Set-Cookie] بواسطة خادم Flask. تُسمى قيمته «ملف تعريف ارتباط الجلسة». وتتكون من ثلاثة عناصر:
    • [session=valeur]: تمثل هذه القيمة ذاكرة المستخدم في شكل مشفر. ويمكن فك تشفير هذه الذاكرة (انظر |https://blog.miguelgrinberg.com/post/how-secure-is-the-flask-user-session|). ومع ذلك، وبسبب المفتاح السري الذي يستخدمه الخادم، لا يمكن للمستخدم تعديل الذاكرة المستلمة لإعادة إرسالها بعد ذلك إلى الخادم. وعندما يتلقى الخادم جلسة عمل، فإنه يتأكد بذلك من تلقي جلسة عمل سليمة؛
    • [HttpOnly]: يشير وجود هذا العنصر للمتصفح الذي يستلمه إلى أن ملف تعريف الارتباط يجب ألا يكون متاحًا لـ JavaScript الذي قد تحتوي عليه الصفحة التي يعرضها؛
    • [Path=/] هو المسار الذي يجب إعادة ملف تعريف ارتباط الجلسة إليه، أي في هذه الحالة أي مسار في تطبيق الويب. في كل مرة يطلب فيها المستخدم عبر لوحة المفاتيح بشكل صريح (عند كتابة URL) أو ضمني (عند النقر على رابط) URL من هذا النطاق، سيقوم المتصفح تلقائيًا بإعادة إرسال ملف تعريف ارتباط الجلسة الذي تلقّاه؛

عيب النافذة الرئيسية هو عدم إمكانية الوصول إلى الطلب الكامل الذي أدى إلى هذا الرد. ما يُعرض في هذه النافذة يسبب الالتباس:

Image

  • في الرؤوس HTTP [3-4]، يتم عرض ملف تعريف ارتباط الجلسة [5]. قد يظن المرء إذن أن Postman قد أدرج ملف تعريف ارتباط الجلسة في الطلب، في حين أن هذا ليس صحيحًا. تمثل رؤوس القسم [3] في الواقع رؤوس القسم HTTP التي سيتم إرسالها عند الطلب التالي وفقًا للإعدادات الحالية. لقد تلقى Postman للتو ملف تعريف ارتباط جلسة عمل سيقوم بإرساله مرة أخرى في الطلب التالي. ولهذا السبب لدينا [5]؛

يمكننا الوصول إلى حوار العميل/الخادم في وحدة التحكم في Postman، والذي يمكننا فتحه باستخدام Ctrl-Alt-C:


GET /set-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 3673b73f-7600-4df4-8c4b-c37973e50df8
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 0
Vary: Cookie
Set-Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:32:57 GMT
  • السطر 14: ملف تعريف الارتباط الخاص بالجلسة الذي أرسله الخادم؛

الآن لنطلب URL [/get-session]:

GET /get-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: ce991398-2d9a-46d0-9ccd-c7ff3c7f4d6d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 20
Vary: Cookie
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:36:52 GMT

{"nom": "séléné"}
  • السطر 9: أرسل عميل Postman ملف تعريف الارتباط الخاص بالجلسة الذي تلقّاه إلى الخادم؛
  • السطر 18: السلسلة jSON المرسلة من الخادم؛

يوضح لنا هذا المثال عدة نقاط:

  • يقوم عميل Postman بإعادة إرسال ملف تعريف الارتباط الخاص بالجلسة الذي يتلقاه من خادم Flask. وتقوم متصفحات الويب دائمًا بذلك؛
  • نلاحظ أن الطلب 2 [/get-session] سمح باسترداد معلومة تم إنشاؤها خلال الطلب 1 [/set-session]. وبذلك يكون لدينا ذاكرة خاصة بالمستخدم؛
  • الأسطر 11-16: لم يقم خادم Flask بإرجاع ملف تعريف ارتباط الجلسة. وهذا الأمر ليس منهجيًا. لا يقوم خادم Flask بإرجاع ملف تعريف ارتباط الجلسة إلا إذا كان الطلب الأخير قد أدى إلى تعديل ذاكرة المستخدم؛

22.6.3. البرنامج النصي [session_scope_02]

Image

النص البرمجي [session_02] هو كما يلي:


# التبعيات
import os

from flask import Flask, make_response, session
from flask_api import status

# تطبيق Flask
app = Flask(__name__)

# المفتاح السري للجلسة
app.secret_key = os.urandom(12).hex()


# الصفحة الرئيسية URL
@app.route('/', methods=['GET'])
def index():
    # نقوم بإدارة ثلاثة عدادات
    if session.get('n1') is None:
        session['n1'] = 0
    else:
        session['n1'] = session['n1'] + 1
    if session.get('n2') is None:
        session['n2'] = 10
    else:
        session['n2'] = session['n2'] + 1
    if session.get('n3') is None:
        session['n3'] = 100
    else:
        session['n3'] = session['n3'] + 1
    # قاموس العدادات
    compteurs = {"n1": session['n1'], "n2": session['n2'], "n3": session['n3']}
    # إرسال الرد
    response = make_response(compteurs)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 11: هنا يتم إنشاء المفتاح السري باستخدام دالة. وتكمن فائدة هذه الدالة في أنها تولد سلسلة أحرف معقدة بشكل عشوائي. تجدر الإشارة إلى أن المتغير [app] هو مثيل فئة Flask الذي تم إنشاؤه في السطر 8؛
  • السطر 15: هذه المرة، سيكون هناك مسار واحد فقط، وهو المسار /؛
  • الأسطر 17-29: يتم إدارة جلسة تحتوي على ثلاثة عدادات [n1, n2, n3]. عند أول استدعاء من المستخدم، يكون [n1, n2, n3]=[0, 10, 100]، ثم يتم زيادة هذه العدادات بمقدار 1 عند كل استدعاء؛
  • السطر 18: عند الطلب الأول، تكون جلسة عمل التطبيق فارغة. يُرجع التعبير [session.get(‘clé’)] القيمة [None]. بالنسبة للطلبات التالية، سيُرجع هذا التعبير القيمة المرتبطة بالمفتاح؛
  • السطر 31: يتم وضع هذه العدادات في قاموس؛
  • السطر 33: هذا القاموس هو مستند الرد HTTP. تجدر الإشارة إلى أن Flask يحول القواميس تلقائيًا إلى سلسلة jSON؛
  • السطر 34: يتم إخطار عميل الويب بأنه سيتلقى jSON؛
  • السطر 35: يتم إرسال الرد HTTP إلى العميل؛

لنقم بتنفيذ هذا البرنامج النصي ونستعلم عن التطبيق الويب الذي تم إنشاؤه بهذه الطريقة باستخدام Postman بعد حذف جميع ملفات تعريف الارتباط من عميل Postman [1-3]:

Image

في وحدة التحكم Postman، تتم عمليات التبادل بين العميل والخادم على النحو التالي:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: c7db536d-9352-4aa6-9877-04560e03d935
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:50:22 GMT

{
"n1": 0, 
"n2": 10, 
"n3": 100
}
  • في [14]، ملف تعريف الارتباط الخاص بالجلسة الذي أرسله الخادم؛
  • في [18-22]، رد الخادم على شكل سلسلة jSON؛

لنكرر نفس الطلب مرة ثانية. تتغير السجلات على النحو التالي:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 8205ad85-37b3-41f2-a171-70dd3b3a1679
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MSwibjIiOjExLCJuMyI6MTAxfQ.Xw6nsw.OuxIQnGhmhSsan5Qu_FL3Iyu-9k; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:52:35 GMT

{
"n1": 1, 
"n2": 11, 
"n3": 101
}
  • السطر 9: يقوم عميل Postman بإعادة إرسال ملف تعريف الارتباط الخاص بالجلسة الذي تلقّاه؛
  • السطر 15: في رده، يرسل الخادم ملف تعريف ارتباط جلسة جديد، وذلك لأن طلب العميل قد غيّر ذاكرة المستخدم (= الجلسة)؛
  • الأسطر 19-23: القيم الجديدة للعدادات؛

22.6.4. البرنامج النصي [session_scope_03]

يهدف هذا البرنامج النصي الجديد إلى إظهار أنه يمكن وضع أنواع مختلفة من Python في جلسة العمل: قائمة، قاموس، كائن. الشرط الوحيد هو أن تكون الكائنات الموضوعة في الجلسة قابلة للتسلسل في jSON. وإذا لم تكن كذلك افتراضيًا (القوائم، القواميس)، فيجب عندئذٍ إجراء التحويل بنفسك في jSON.


# يتم تكوين التطبيق
import config
config = config.configure()

# التبعيات
import json
import os

from flask import Flask, make_response, session
from flask_api import status
from Personne import Personne

# تطبيق Flask
app = Flask(__name__)

# المفتاح السري للجلسة
app.secret_key = os.urandom(12).hex()


# الصفحة الرئيسية URL
@app.route('/', methods=['GET'])
def index():
    # إدارة قائمة
    liste = session.get('liste')
    if liste is None:
        # الطلب الأول
        liste = [0, 10, 100]
    else:
        # الطلبات التالية
        for i in range(len(liste)):
            liste[i] += 1
    # إعادة إدراج القائمة في الجلسة
    session['liste'] = liste

    # إدارة قاموس
    dico = session.get('dico')
    if not dico:
        # الاستعلام الأول
        dico = {"un": 0, "deux": 10, "trois": 100}
    else:
        # الطلبات التالية
        dico = session['dico']
        for key in dico.keys():
            dico[key] += 1
    # إعادة القاموس إلى الجلسة
    session['dico'] = dico

    # إدارة شخص
    personne_json = session.get('personne')
    if personne_json is None:
        # الطلب الأول
        personne = Personne().fromdict({"prénom": "aglaë", "nom": "séléné", "âge": 70})
    else:
        # الطلبات التالية
        personne = Personne().fromjson(personne_json)
        personne.âge += 1
    # إعادة إدراج الشخص في الجلسة
    session['personne'] = personne.asjson()

    # قاموس النتائج
    résultats = {"liste": liste, "dict": dico, "personne": personne.asdict()}

    # إرسال رد jSON
    response = make_response(json.dumps(résultats, ensure_ascii=False))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • الأسطر 1-3: يتم تكوين تطبيق الويب؛
  • الأسطر 5-11: يتم استيراد التبعيات؛
  • السطر 14: يتم إنشاء مثيل لتطبيق Flask؛
  • السطر 17: يتم تهيئة السمة [secret_key]. وهذا ما يسمح باستخدام الجلسات؛
  • السطر 21: المسار الوحيد للتطبيق؛
  • الأسطر 23-33: إدارة قائمة في الجلسة. تم وضع عناصر قابلة للتسلسل افتراضيًا في هذه القائمة باستخدام jSON؛
  • الأسطر 35-46: إدارة قاموس في الجلسة. تم إدراج عناصر قابلة للتسلسل افتراضيًا في jSON؛
  • الأسطر 48-58: إدارة شخص. الكائن [Personne] غير قابل للتسلسل افتراضيًا في jSON. لذا يجب اتخاذ الاحتياطات اللازمة؛
  • السطر 58: نستخدم الطريقة [BaseEntity.asjson] لتخزين السلسلة jSON الخاصة بالشخص في الجلسة. تجدر الإشارة إلى أنه كان من الممكن استخدام [personne.asdict] لأن [personne.asdict] عبارة عن قاموس يحتوي على قيم قابلة للتسلسل افتراضيًا إلى jSON؛
  • السطر 55: نظرًا لأننا قمنا بتخزين السلسلة jSON في الجلسة، فإننا نسترد الشخص منها باستخدام الطريقة [BaseEntity.fromjson
  • السطر 61: يتم إنشاء القاموس [résultats] الذي سيتم إرساله كاستجابة إلى العميل. نعلم أنه في هذه الحالة، يرسل Flask السلسلة jSON من القاموس. لذا يجب ألا يحتوي هذا القاموس إلا على قيم قابلة للتسلسل افتراضيًا في jSON؛
  • السطر 64: نضع صراحةً السلسلة jSON من القاموس [résultats] في الرد HTTP. وكان Flask سيفعل ذلك افتراضيًا. إلا أنه، بشكل افتراضي أيضًا، يستخدم المعلمة [ensure_ascii=True]، وهو ما لم يكن مناسبًا لنا؛
  • السطر 65: نُعلم العميل بأنه سيتلقى jSON؛
  • السطر 66: نرسل له الرد؛

نقوم بتشغيل تطبيق الويب. نحذف جميع ملفات تعريف الارتباط من عميل Postman. ثم يطلب هذا الأخير URL [http://localhost:5000]. الحوار بين العميل والخادم في وحدة التحكم Postman هو كما يلي:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5f8b7c63-aa8a-4429-a2fa-62141423d933
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:36:59 GMT

{"liste": [0, 10, 100], "dict": {"un": 0, "deux": 10, "trois": 100}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 70}}

نقوم بإرسال الطلب مرة ثانية:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 40fd00ea-d45c-46b7-a51e-d4d433a37b5c
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkP2kupv9LtIW2WIBgNGqEg_nu3seQ0b2Zew-zfCa5hlvqBs5aw5-SLolGuUaETgi-7wD0sqaHPk7BJLilGXdEYW-ZqjNxjWhnuwpiWMB3Ti0Haz6MMMfz9EcM5-LrIT7zZjv4F5NYvOQ.Xw6ydQ.PMWRCqKx9HNnb_DyK-ha-9pCF7M; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:38:29 GMT

{"liste": [1, 11, 101], "dict": {"deux": 11, "trois": 101, "un": 1}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 71}}
  • السطر 9: يقوم العميل بإعادة إرسال ملف تعريف الارتباط الخاص بالجلسة الذي تلقّاه؛
  • السطر 15: يقوم الخادم بإرسال ملف تعريف ارتباط آخر إليه لأن محتوى الجلسة قد تغير (السطر 19). تجدر الإشارة إلى أن هذا المحتوى موجود في ملف تعريف ارتباط الجلسة في شكل مشفر؛

22.7. نصوص برمجية [flask/06]: معلومات مشتركة بين جميع المستخدمين

22.7.1. مقدمة

يهدف هذا القسم إلى توضيح كيفية إدارة المعلومات ذات النطاق الخاص بالتطبيق، أي تلك التي يتم مشاركتها بين جميع المستخدمين. وعادةً ما تكون هذه المعلومات عبارة عن معلومات تكوين التطبيق. وقد رأينا أن تطبيق الويب يمكنه الاحتفاظ بأنواع مختلفة من الذاكرة:

Image

ونركز هنا على ذاكرة التطبيق [3].

22.7.2. نص برمجي [application_scope_01]

Image

يوضح البرنامج النصي [application_scope_01] إحدى طرق إدارة البيانات ذات نطاق «التطبيق»:


# يتم تكوين التطبيق
import config
config = config.configure()

# التبعيات
from flask import Flask, make_response
from flask_api import status

# تطبيق Flask
app = Flask(__name__)


# الصفحة الرئيسية URL
@app.route('/', methods=['GET'])
def index():
    # نهدف إلى إظهار أن التطبيق يبقى في الذاكرة بين طلبات العملاء المختلفين
    # يتعامل كل عميل مع نفس التطبيق

    # يمثل app_infos معلومات على مستوى التطبيق وليس على مستوى الجلسة
    # أي أنها تتعلق بجميع المستخدمين وليس بمستخدم معين
    # يتم تخزين هذه المعلومات هنا في [config] (ليس إلزاميًا)

    # قاموس النتائج
    résultats = {"config": config}

    # يتم إرسال الرد
    response = make_response(résultats)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# اليد
if __name__ == '__main__':
    # التحقق مما إذا كان هذا الرمز يُنفَّذ عدة مرات
    print("application app lancée")
    # تشغيل تطبيق الويب
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • الأسطر 1-3: يتم استرداد قاموس التكوين. سنوضح أن الكود الموجود خارج وظائف التوجيه لا يتم تنفيذه إلا مرة واحدة. يبقى تطبيق Flask في الذاكرة. جميع المعلومات التي يتم تهيئتها خارج المسارات تكون عامة بالنسبة لهذه المسارات، وبالتالي فهي معروفة لها. وبالتالي، سيتم عرض القاموس [config] الموجود في السطر 3 بواسطة مسار / (السطر 24). سنوضح أن جميع عملاء الويب سيتلقون نفس القاموس، وبالتالي فإن هذا القاموس مشترك بين جميع العملاء. لذا فهي معلومات ذات نطاق «التطبيق»؛
  • السطر 35: نضع سجلًا لمعرفة ما إذا كان كود الأسطر خارج وظيفة التوجيه (الأسطر 1-10، 32-38) يُنفَّذ عدة مرات؛

التكوين [config] هو كما يلي:


def configure():
    # تقديم التكوين
    config = {
        # تكوين Flask
        "SECRET_KEY""vibnFfrdWYUp?*LQ"
    }

    return config

نقوم بتشغيل هذا التطبيق. وفيما يلي سجلات وحدة التحكم PyCharm:

Image

  • في [1]، التشغيل الأولي للتطبيق؛
  • في [2]، نظرًا لطلب وضع [Debug]، يتم إعادة تشغيل التطبيق في وضع [Debug]؛

الآن باستخدام متصفح (Chrome أدناه)، نطلب URL [http://127.0.0.1:5000/]:

Image

الآن باستخدام متصفح Firefox:

Image

الآن باستخدام عميل Postman:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 51e75099-8ecb-4f27-ae3b-9386e982ede4
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 39
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 10:34:26 GMT

{
"SECRET_KEY": "vibnFfrdWYUp?*LQ"
}

الآن، نعود إلى وحدة التحكم [Run] في Pycharm:

Image

  • لا يزال السجلان [1, 2] موجودين ولكن لا يوجد سجلات أخرى، في حين نرى الطلبات الثلاثة التي استقبلها خادم الويب؛

وللتأكد تمامًا من أن التطبيق لا يُعاد تحميله مع كل طلب جديد، يمكننا وضع عداد في الإعدادات وزيادته مع كل طلب جديد. وسنلاحظ عندئذٍ أن كل عميل يرى العداد في الحالة التي تركه عليها العميل السابق. ومع ذلك، تجدر الإشارة إلى أنه لا ينبغي للعملاء تعديل البيانات ذات النطاق التطبيقي لأنها مشتركة بين جميع العملاء، وفي سياق يخدم فيه الخادم عدة عملاء في وقت واحد دون ضمان تنفيذ طلب أحد العملاء بالكامل دون مقاطعة، فإن العميل 1 الذي أرسل الطلب 1 الذي تمت مقاطعته قبل نهايته قد يترك البيانات المشتركة في حالة تالفة بالنسبة للعملاء التاليين.

22.7.3. نص برمجي [application_scope_02]

Image

سيقوم البرنامج النصي [application_scope_02] بما لا ينبغي فعله: السماح للعملاء بتعديل المعلومات المشتركة مع المستخدمين الآخرين. سنقوم بمشاركة عداد بين المستخدمين الذين سيقومون بزيادته. سنرى أن كل مستخدم يرى التعديلات التي أجراها المستخدمون الآخرون على العداد.

النص البرمجي هو كما يلي:


# التبعيات

from flask import Flask, make_response
from flask_api import status

# تطبيق Flask
app = Flask(__name__)

# بيانات نطاق التطبيق
config = {
    "counter": 0
}


# الصفحة الرئيسية URL
@app.route('/', methods=['GET'])
def index():
    # نهدف إلى إظهار أن القاموس [config] مشترك بين جميع العملاء
    # لتطبيق الويب

    # يتم زيادة العداد
    config["counter"] += 1
    # يتم إرسال الرد
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • الأسطر 10-12: القاموس [config] المشترك بين المستخدمين. وهو يحتوي على عداد؛
  • السطر 22: في كل مرة يطلب فيها مستخدم URL /، سيتم زيادة عداد التكوين؛
  • الأسطر 23-26: يتم إرسال السلسلة jSON من القاموس إلى كل عميل؛

نقوم بتشغيل هذا البرنامج النصي. ثم نطلب URL [http://127.0.0.1:5000/] باستخدام متصفح أول:

Image

ثم نكرر نفس الإجراء باستخدام متصفح ثانٍ:

Image

ثم للمرة الثالثة باستخدام Postman:

Image

نلاحظ أن كل عميل يسترد العداد في الحالة التي تركه فيها العميل السابق. وبالتالي، فإنهم يتمتعون بالوصول إلى نفس المعلومات.

22.7.4. البرنامج النصي [application_scope_03]

يوضح البرنامج النصي [application_scope_03] سبب ضرورة أن تكون المعلومات المشتركة بين المستخدمين للقراءة فقط.

Image

النص البرمجي هو كما يلي:


# التبعيات
import threading
from time import sleep

from flask import Flask, make_response
from flask_api import status

# تطبيق Flask
app = Flask(__name__)

# بيانات نطاق التطبيق
config = {
    "counter": 0
}


# الصفحة الرئيسية URL
@app.route('/', methods=['GET'])
def index():
    # نهدف إلى إظهار أن القاموس [config] مشترك بين جميع العملاء
    # لتطبيق الويب وأنه يجب أن يكون للقراءة فقط

    # اسم مؤشر الترابط
    thread_name = threading.current_thread().name
    # يتم قراءة العداد
    counter = config["counter"]
    print(f"compteur lu : {counter}, par le thread {thread_name}")
    # التوقف لمدة 5 ثوانٍ - وبالتالي سيتم خدمة عملاء آخرين
    sleep(5)
    # يتم زيادة عداد التكوين
    config["counter"] = counter + 1
    # السجل
    print(f"compteur écrit : {config['counter']}, par le thread {thread_name}")
    # إرسال الرد
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • السطر 43: تم تغيير وضع تشغيل تطبيق الويب. تمت كتابة [threaded=True] للإشارة إلى أن التطبيق يجب أن يخدم المستخدمين في وقت واحد. ويتم ذلك عن طريق خيوط التنفيذ:
    • قد يكون هناك عدة خيوط تنفيذ متزامنة، كل منها يخدم مستخدمًا واحدًا؛
    • يتم تقاسم معالج الجهاز بين هذه الخيوط؛
    • قد يتم مقاطعة خيط ما قبل أن ينتهي من عمله. وسيتم استئنافه لاحقًا؛
  • السطر 19: يمكن تنفيذ الدالة [index] في وقت واحد بواسطة عدة خيوط؛
  • السطر 24: يتم استرداد اسم الخيط الذي ينفذ الدالة [index]؛
  • السطر 26: يتم قراءة قيمة العداد. ولأغراض هذا العرض التوضيحي، نقوم بتفصيل عملية زيادة العداد على النحو التالي:
    • الخطوة 1: قراءة العداد (1 على سبيل المثال) بواسطة الخيط 1؛
    • الخطوة 2: توقف الخيط 1 لمدة 5 ثوانٍ (السطر 29). ونظرًا لأن الخيط 1 طلب التوقف مؤقتًا، يُمنح المعالج لخيط آخر، وهو الخيط 2. والهدف هو أن يقرأ هذا الخيط الجديد نفس قيمة العداد (=1). ثم يتوقف هو أيضًا لمدة 5 ثوانٍ ويفقد المعالج؛
    • الخطوة 3: زيادة قيمة العداد، السطر 31، انطلاقًا من القيمة التي تمت قراءتها في الخطوة 1 (=1). الخيط 1 هو أول من يقوم بذلك: فهو يرفع قيمة العداد إلى 2 ثم ينهي تنفيذ الدالة [index]. ثم يأتي دور الخيط 2 للاستيقاظ ويقوم هو أيضًا بزيادة العداد إلى 2 انطلاقًا من القيمة التي تمت قراءتها في الخطوة 1 (=1). في النهاية، بعد مرور الخيطين، يكون العداد عند 2 في حين أنه من المفترض أن يكون عند 3؛
  • السطر 33: يتم عرض قيمة العداد للتحقق؛

نقوم بتشغيل البرنامج النصي ثم نطلب عنوان URL [http://loaclhost :5000/] باستخدام متصفحين ثم باستخدام Postman. تكون السجلات في وحدة التحكم PyCharm كما يلي:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/flask/06/application_scope_03.py
 * Serving Flask app "application_scope_03" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (اضغط على CTRL+C للخروج)
compteur lu : 0, par le thread Thread-2
compteur lu : 0, par le thread Thread-4
compteur écrit : 1, par le thread Thread-2
127.0.0.1 - - [16/Jul/2020 08:55:37] "GET / HTTP/1.1" 200 -
compteur écrit : 1, par le thread Thread-4
127.0.0.1 - - [16/Jul/2020 08:55:40] "GET / HTTP/1.1" 200 -
compteur lu : 1, par le thread Thread-5
compteur écrit : 2, par le thread Thread-5
127.0.0.1 - - [16/Jul/2020 08:55:46] "GET / HTTP/1.1" 200 -
  • السطران 9-10: يقرأ الخيطان الأولان 2 و4 نفس القيمة 0 للعداد؛
  • السطر 11: يقوم الخيط 2 بضبط العداد على 1؛
  • السطر 13: يقوم الخيط 4 بتغيير قيمة العداد إلى 1. ومن الآن فصاعدًا، تصبح قيمة العداد غير صحيحة؛
  • السطران 15-16: لا يتم مقاطعة الخيط 5 ويقوم بمعالجة قيمة العداد بشكل صحيح؛

من هذا المثال، نستخلص أن كود تطبيق الويب يجب ألا يغير قيمة المعلومات التي يشاركها المستخدمون.

22.8. نصوص برمجية [flask/07]: إدارة الطرق

Image

نركز هنا على إدارة مسارات التطبيق، أي URL التي يقدمها تطبيق الويب.

22.8.1. النص البرمجي [main_01]: المسارات المُعدة

يقدم البرنامج النصي [main_01] إمكانية تكوين المسارات:


from flask import Flask, make_response
from flask_api import status

# تطبيق Flask
app = Flask(__name__)


# إرسال الرد
def send_plain_response(réponse: str):
    # يتم إرسال الرد
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


# /الاسم/الاسم الأول
@app.route('/<string:nom>/<string:prenom>', methods=['GET'])
def index(nom, prenom):
    # الرد
    return send_plain_response(f"{prenom} {nom}")


# بدء الجلسة
@app.route('/init-session/<string:type>', methods=['GET'])
def init_session(type: str):
    # الرد
    return send_plain_response(f"/init-session/{type}")


# توثيق المستخدم
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur():
    # الرد
    return send_plain_response("/authentifier-utilisateur")


# حساب الضريبة
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot():
    # الرد
    return send_plain_response("/calculer-impot")


# عرض قائمة المحاكاة
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations():
    # الإجابة
    return send_plain_response("/lister-simulations")


# حذف-المحاكاة
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int):
    # الرد
    return send_plain_response(f"/supprimer-simulation/{numero}")


# إنهاء-الجلسة
@app.route('/fin-session', methods=['GET'])
def fin_session():
    # الرد
    return send_plain_response(f"/fin-session")


# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 17: نحدد نوع معلمات URL. وهذا يسمح لـ Flask بإجراء عمليات التحقق. إذا لم يكن نوع المعلمة هو النوع المتوقع، فسيتم رفض طلب العميل (خطأ 400 Bad Request). وبالتالي، يقوم Flask بجزء من العمل الذي كان من المفترض أن نقوم به نحن؛
  • السطر 18: بالنسبة للمعلمات، يجب استخدام الأسماء الدقيقة للمعلمات الواردة في السطر 17، ولكن ليس بالضرورة ترتيبها؛
  • السطر 20: نستخدم الدالة [send_plain_response] لإرسال الاستجابة إلى عميل الويب؛
  • السطر 9: تستقبل الدالة [send_plain_response] سلسلة الأحرف المراد إرسالها إلى العميل؛
  • السطر 11: يتم إنشاء نص الرد HTTP؛
  • السطر 12: يتم إخطار العميل بأنه سيتم إرسال نص عادي إليه؛
  • السطر 13: يتم إرسال الرد HTTP؛
  • الأسطر 23-62: مسارات أخرى تم تكوينها وسيتم استخدامها لاحقًا في تمرين تطبيقي؛

يتم تشغيل البرنامج النصي واستدعائه باستخدام عميل Postman:

Image

22.8.2. البرنامج النصي [main_02]: فصل المسارات

في البرنامج النصي [main_01] السابق، قد يصبح حجم الكود كبيرًا في حالة وجود العديد من المسارات. يوضح البرنامج النصي [main_02] كيفية فصل المسارات.

Image

يجمع البرنامج النصي [routes_02] الوظائف المرتبطة بالطرق من البرنامج النصي السابق:


from flask import make_response
from flask_api import status


def send_response(réponse: str):
    # إرسال الرد
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


# الصفحة الرئيسية URL
def index(nom, prenom):
    # الرد
    return send_response(f"{prenom} {nom}")


# بدء الجلسة
def init_session(type: str):
    # الرد
    return send_response(f"/init-session/{type}")


# توثيق المستخدم
def authentifier_utilisateur():
    # الرد
    return send_response("/authentifier-utilisateur")


# حساب الضريبة
def calculer_impot():
    # الرد
    return send_response("/calculer-impot")


# عرض قائمة المحاكاة
def lister_simulations():
    # الرد
    return send_response("/lister-simulations")


# حذف-المحاكاة
def supprimer_simulation(numero: int):
    # الرد
    return send_response(f"/supprimer-simulation/{numero}")


# إنهاء-الجلسة
def fin_session():
    # الرد
    return send_response(f"/fin-session")

تجدر الإشارة إلى أن البرنامج النصي [routes_02] ليس برنامجًا نصيًّا للطرق. إنه قائمة بالوظائف. البرنامج النصي الرئيسي [main_02] هو الذي يربط بين الطرق والوظائف:


from flask import Flask

# ننقل وظائف المسارات إلى نصوصها البرمجية الخاصة
import routes_02

# تطبيق Flask
app = Flask(__name__)

# الربط بين المسارات والوظائف
app.add_url_rule('/<string:nom>/<string:prenom>', methods=['GET'], view_func=routes_02.index)
app.add_url_rule('/init-session/<string:type>', methods=['GET'], view_func=routes_02.init_session)
app.add_url_rule('/authentifier-utilisateur', methods=['POST'], view_func=routes_02.authentifier_utilisateur)
app.add_url_rule('/calculer-impot', methods=['POST'], view_func=routes_02.calculer_impot)
app.add_url_rule('/lister-simulations', methods=['GET'], view_func=routes_02.lister_simulations)
app.add_url_rule('/supprimer-simulation/<int:numero>', methods=['GET'], view_func=routes_02.supprimer_simulation)
app.add_url_rule('/fin-session', methods=['GET'], view_func=routes_02.fin_session)

# الرئيسية
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • السطر 4: يتم استيراد البرنامج النصي للوظائف المرتبطة بالمسارات؛
  • الأسطر 9-16: ربط المسارات بالوظائف؛

باستخدام هذه الطريقة، يمكن أن تكون كل وظيفة مرتبطة بمسار موضوعًا لبرنامج نصي منفصل إذا لزم الأمر.

والنتائج هي نفسها التي تم الحصول عليها باستخدام البرنامج النصي [main_01] السابق.