23. تمرين تطبيقي: الإصدار 6
23.1. مقدمة
نعود الآن إلى تطبيقنا لحساب الضريبة. سنقوم ببناء تطبيقات ويب مختلفة حوله.
في الإصدار 5 من تمريننا التطبيقي، كانت بيانات مصلحة الضرائب مخزنة في قاعدة بيانات. تضمن هذا الإصدار 5 تطبيقين منفصلين ولكنهما يشتركان في بعض الطبقات:
- تطبيق يحسب الضريبة في الوضع |batch| للمكلفين المسجلين في ملف نصي؛
- تطبيق يحسب الضريبة في الوضع |التفاعلي| للمكلفين الذين يتم إدخال معلوماتهم عبر لوحة المفاتيح؛
كانت النسخة 5 من تطبيق حساب الضريبة على دفعات (وضع batch) تتمتع بالبنية التالية:

وفي النهاية، ستتخذ النسخة الويب من هذا التطبيق البنية التالية:

- يتصل عميل الويب [1] بخادم الويب [2] الذي يتواصل بدوره مع SGBD و[3]؛
- يحتفظ خادم الويب [2] بالطبقات [métier] و[8] و[dao] و[9] من التطبيق الأصلي؛
- يحتفظ التطبيق الأصلي ببرمجته النصية الرئيسية [4] وطبقته [métier] [15]. الطبقات [métier] و[8] و[15] متطابقة؛
- يتطلب الاتصال بين العميل والخادم طبقتين إضافيتين:
- الطبقة [web] [7] التي تنفذ تطبيق الويب؛
- الطبقة [dao] [5] التي تعمل كعميل لتطبيق الويب [7]؛
في الإصدار النهائي، يمكن إجراء حساب الضريبة على دفعات بطريقتين:
- يتم الحساب الفني للضريبة عبر الطبقة [métier] على الخادم. وسيستخدم البرنامج النصي [main] هذه الطريقة؛
- يتم حساب الضريبة وفقًا للمعايير المهنية عبر الطبقة [métier] الخاصة بالعميل. وسيستخدم البرنامج النصي [main2] هذه الطريقة؛
من الآن فصاعدًا، سنقوم بتطوير العديد من تطبيقات العميل/الخادم من النوع المذكور أعلاه، حيث يوضح كل منها تقنية أو تقنيات جديدة لتطوير الويب.
23.2. خادم الويب لحساب الضريبة
23.2.1. الإصدار 1

البرنامج النصي [server_01] هو تطبيق الويب التالي:

- في [1]، يتم استخدام URL معلمات يتم تمرير ثلاث قيم إليها:
- [marié] (نعم / لا) للإشارة إلى ما إذا كان المكلف متزوجًا أم لا؛
- [enfants]: عدد أطفال المكلف؛
- [salaire]: الراتب السنوي للمكلف؛
- في [2]، يقوم خادم الويب بإرجاع سلسلة jSON التي تحدد مبلغ الضريبة المستحقة مع مكوناتها المختلفة؛
بنية التطبيق هي كما يلي:

- يقوم المتصفح [1] باستعلام الخادم [2]. يقوم البرنامج النصي [server_01] بتنفيذ الطبقة [web] [2] الخاصة بالخادم؛
- الطبقات [3-8] هي تلك المستخدمة بالفعل في |الإصدار 5| من تطبيق حساب الضريبة. ونعيد استخدامها كما هي؛
- يتم تعريف الطبقة [métier] [3] |هنا|؛
- يتم تعريف الطبقة [dao] [4] |هنا|؛
يتم تكوين تطبيق الويب [server_01] باستخدام ثلاثة نصوص برمجية:
- [config] الذي يقوم بتكوين التطبيق بالكامل؛
- [config_database] الذي يقوم بتكوين الوصول إلى قاعدة البيانات. وسنعمل مع SGBD وMySQL وPostgreSQL؛
- [config_layers] الذي يقوم بتكوين طبقات التطبيق؛
النص البرمجي [config] هو كما يلي:
def configure(config: dict) -> dict:
import os
# الخطوة 1 ------
# مجلد هذا الملف
script_dir = os.path.dirname(os.path.abspath(__file__))
# المسار الجذري
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# التبعيات المطلقة
absolute_dependencies = [
# مجلدات المشروع
# BaseEntity، MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao، InterfaceImpôtsMétier، InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao، ImpôtsConsole، ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData، ImpôtsError، TaxPayer
f"{root_dir}/impots/v04/entities",
# الثوابت، الشرائح
f"{root_dir}/impots/v05/entities",
# IndexController
f"{script_dir}/../controllers",
# البرامج النصية [config_database, config_layers]
script_dir,
]
# تحديد مسار النظام
from myutils import set_syspath
set_syspath(absolute_dependencies)
# الخطوة 2 ------
# تكوين التطبيق
# قائمة المستخدمين المصرح لهم باستخدام التطبيق
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
# الخطوة 3 ------
# تكوين قاعدة البيانات
import config_database
config["database"] = config_database.configure(config)
# الخطوة 4 ------
# إنشاء مثيلات طبقات التطبيق
import config_layers
config['layers'] = config_layers.configure(config)
# تطبيق التكوين
return config
- تستقبل الدالة [configure] قاموسًا [config] كمعلمة (السطر 1) وتُرجعه كنتيجة (السطر 54) بعد إثراء محتواه. كان من الممكن القول منذ وقت طويل إنه ليس من الضروري إرجاع النتيجة [config]. ففي الواقع، [config] هي مرجع قاموس يشترك فيه كل من الكود المستدعي والكود المستدعى. وبالتالي، فإن الكود المستدعي يمتلك هذه الإشارة بالفعل (السطر 1) ولا داعي لإعادة تزويده بها (السطر 54). لذا، يمكن كتابة:
config=[module].configure(config) (1)
يعد تكرارًا. يكفي كتابة:
[module].configure(config) (2)
ومع ذلك، فقد احتفظت بنمط الكتابة (1) لأنني اعتقدت أنه ربما يوضح بشكل أفضل أن الكود المستدعى يقوم بتعديل القاموس [config].
- السطر 1: القاموس [config] الذي استقبلته الدالة [configure] يحتوي على مفتاح «sgbd» الذي يأخذ قيمته من القائمة [‘mysql’, ‘pgres’]. [mysql] تعني أن قاعدة البيانات المستخدمة تدار بواسطة MySQL، بينما تعني «pgres» أن قاعدة البيانات المستخدمة تدار بواسطة PostgreSQL؛
- الأسطر 4-27: يتم سرد جميع المجلدات التي تحتوي على العناصر الضرورية لتطبيق الويب. وستكون هذه المجلدات جزءًا من مسار Python الخاص بالتطبيق (الأسطر 30-31)؛
- الأسطر 33-40: لن نسمح إلا لبعض المستخدمين بالوصول إلى التطبيق. وهنا لدينا قائمة تضم مستخدمًا واحدًا؛
- الأسطر 43-46: يقوم البرنامج النصي [config_database] بإنشاء تكوين قاعدة البيانات المستخدمة؛
- السطر 46: التكوين الذي أنشأه البرنامج النصي [config_database] هو قاموس يتم تخزينه في التكوين العام المرتبط بالمفتاح «database»؛
- الأسطر 48-51: يقوم البرنامج النصي [config_layers] بإنشاء مثيلات لطبقات تطبيق الويب. ويُرجع قاموسًا يتم تخزينه في التكوين العام المرتبط بالمفتاح «layers»؛
البرنامج النصي [config_database] هو نفسه الذي تم استخدامه بالفعل في |الإصدار 5|. نعيد ذكره للتذكير:
def configure(config: dict) -> dict:
# تكوين SQLAlchemy
from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
from sqlalchemy.orm import mapper, sessionmaker
# سلاسل الاتصال بقواعد البيانات المستخدمة
connection_strings = {
'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
}
# سلسلة اتصال قاعدة البيانات المستخدمة
engine = create_engine(connection_strings[config['sgbd']])
# البيانات الوصفية
metadata = MetaData()
# جدول الثوابت
constantes_table = Table("tbconstantes", metadata,
Column('id', Integer, primary_key=True),
Column('plafond_qf_demi_part', Float, nullable=False),
Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
Column('valeur_reduc_demi_part', Float, nullable=False),
Column('plafond_decote_celibataire', Float, nullable=False),
Column('plafond_decote_couple', Float, nullable=False),
Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
Column('plafond_impot_couple_pour_decote', Float, nullable=False),
Column('abattement_dixpourcent_max', Float, nullable=False),
Column('abattement_dixpourcent_min', Float, nullable=False)
)
# جدول شرائح الضريبة
tranches_table = Table("tbtranches", metadata,
Column('id', Integer, primary_key=True),
Column('limite', Float, nullable=False),
Column('coeffr', Float, nullable=False),
Column('coeffn', Float, nullable=False)
)
# التعيينات
from Tranche import Tranche
mapper(Tranche, tranches_table)
from Constantes import Constantes
mapper(Constantes, constantes_table)
# مصنع الجلسات
session_factory = sessionmaker()
session_factory.configure(bind=engine)
# جلسة
session = session_factory()
# يتم تسجيل بعض المعلومات وإرجاعها في قاموس
return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
"constantes_table": constantes_table, "session": session}
يقوم البرنامج النصي [config_layers] بتكوين طبقات خادم الويب. نعيد استخدام |برنامج نصي| سبق ذكره:
def configure(config: dict) -> dict:
# إنشاء مثيلات لطبقات التطبيق
# DAO
from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
dao = ImpotsDaoWithAdminDataInDatabase(config)
# المنطق التجاري
from ImpôtsMétier import ImpôtsMétier
métier = ImpôtsMétier()
# يتم وضع مثيلات الطبقات في قاموس يتم إرجاعه إلى الكود المستدعي
return {
"dao": dao,
"métier": métier
}
- السطر 6: يتم تنفيذ الطبقة [dao] باستخدام قاعدة بيانات؛
- تم تعريف [ImpotsDaoWithAdminDataInDatabase] |هنا|؛
- تم تعريف [ImpôtsMétier] |هنا|؛
النص البرمجي الرئيسي [server_01] هو كما يلي:
# نتوقع معلمة mysql أو pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# يتم تكوين التطبيق
import config
config = config.configure({'sgbd': sgbd})
# التبعيات
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status
# استرداد البيانات من مصلحة الضرائب
try:
# ستكون «admindata» بيانات على نطاق التطبيق للقراءة فقط
admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# تطبيق Flask
app = Flask(__name__)
# الصفحة الرئيسية URL: /?marié=xx&enfants=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
# في البداية لا توجد أخطاء
erreurs = []
# يجب أن تحتوي الطلبات على ثلاثة معلمات في URL
if len(request.args) != 3:
erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")
# يتم استرداد الحالة الاجتماعية من URL
marié = request.args.get('marié')
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
marié = marié.strip().lower()
erreur = marié != "oui" and marié != "non"
if erreur:
erreurs.append(f"paramétre marié [{marié}] invalide")
# يتم استرداد عدد الأبناء من URL
enfants = request.args.get('enfants')
if enfants is None:
erreurs.append("paramètre [enfants] manquant")
else:
enfants = enfants.strip()
match = re.match(r"^\d+", enfants)
if not match:
erreurs.append(f"paramétre enfants [{enfants}] invalide")
else:
enfants = int(enfants)
# يتم استرداد الراتب من URL
salaire = request.args.get('salaire')
if salaire is None:
erreurs.append("paramètre [salaire] manquant")
else:
salaire = salaire.strip()
match = re.match(r"^\d+", salaire)
if not match:
erreurs.append(f"paramétre salaire [{salaire}] invalide")
else:
salaire = int(salaire)
# هل توجد معلمات غير صالحة في URL؟
for key in request.args.keys():
if key not in ['marié', 'enfants', 'salaire']:
erreurs.append(f"paramètre [{key}] invalide")
# هل توجد أخطاء؟
if erreurs:
# يتم إرسال رد خطأ إلى العميل
résultats = {"réponse": {"erreurs": erreurs}}
return json_response(résultats, status.HTTP_400_BAD_REQUEST)
# لا توجد أخطاء، يمكن العمل
# حساب الضريبة
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# يتم إرسال الرد إلى العميل
return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
# يدويًا فقط
if __name__ == '__main__':
# تشغيل خادم Flask
app.config.update(ENV="development", DEBUG=True)
app.run()
- الأسطر 1-10: يتم استرداد المعلمة التي تشير إلى SGBD المطلوب استخدامه؛
- الأسطر 12-14: باستخدام هذه المعلومات، يمكن تكوين التطبيق. ويتم على وجه الخصوص إنشاء مسار Python؛
- الأسطر 16-23: باستخدام مسار Python الجديد، يتم استيراد العناصر المطلوبة؛
- الأسطر 25-31: يتم استرداد البيانات من مصلحة الضرائب التي تسمح بحساب الضريبة؛
- الأسطر 33-34: إنشاء مثيل لتطبيق Flask؛
- السطر 38: لا يخدم تطبيق Flask سوى URL [/]. وهو ينتظر URL مُعدًّا بالطريقة التالية [/ ?marié=xx&enfants=yy&salaire=zz] مع:
- xx: نعم / لا؛
- yy: عدد الأطفال؛
- zz: الراتب السنوي؛
- الأسطر 40-89: يتم التحقق من صحة معلمات URL؛
- السطر 41: سيتم تجميع رسائل الخطأ في القائمة [erreurs]؛
- السطر 43: ربما نتذكر أن معلمات URL المُعدة موجودة في [request.args] (انظر |هنا|):
- الكائن [request] هو كائن Flask الذي تم استيراده في السطر 20؛
- الكائن [request.args] يتصرف كقاموس؛
- السطران 43-44: يتم التحقق من وجود ثلاثة معلمات بالضبط (لا أقل ولا أكثر)؛
- الأسطر 46-49: يتم التحقق من وجود المعلمة [marié] في URL؛
- الأسطر 50-54: إذا كان موجودًا، يتم التحقق من أن قيمته الصغيرة بعد إزالة «المسافات» في البداية والنهاية هي «نعم» أو «لا»؛
- الأسطر 56-59: يتم التحقق من وجود المعلمة [enfants] في المعلمة URL؛
- الأسطر 60-66: إذا كان موجودًا، يتم التحقق من أن قيمته عدد صحيح موجب؛
- السطر 66: يجب ألا ننسى أن معلمات URL وقيمها عبارة عن سلاسل أحرف. يتم تحويل قيمة المعلمة [enfants] إلى نوع «int»؛
- الأسطر 68-78: بالنسبة للمعلمة [salaire]، يتم إجراء نفس الاختبارات التي أجريت على المعلمة [enfants]؛
- الأسطر 81-83: يتم التحقق من عدم وجود معلمات أخرى غير [‘marié, ‘enfants’, ‘salaire’] في URL؛
- الأسطر 85-89: إذا لم تكن قائمة [erreurs] فارغة بعد كل هذه الفحوصات، يتم إرسال قائمة الأخطاء هذه إلى العميل في شكل سلسلة jSON ورمز الحالة [400 Bad Request]؛
ونظرًا لأننا سنضطر في كثير من الأحيان لاحقًا إلى إرسال سلسلة jSON ردًّا على العميل، فقد تم تجميع الأسطر القليلة اللازمة لهذا الإرسال في الوحدة النمطية [myutils.py] التي استخدمناها سابقًا:

يصبح البرنامج النصي [myutils.py] كما يلي:
# الاستيرادات
import json
import os
import sys
from flask import make_response
def set_syspath(absolute_dependencies: list):
# absolute_dependencies: قائمة بأسماء المجلدات المطلقة
….
# إنشاء استجابة HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
# نص الرد HTTP
response = make_response(json.dumps(réponse, ensure_ascii=False))
# نص الرد HTTP هو jSON
response.headers['Content-Type'] = 'application/json; charset=utf-8'
# يتم إرسال الرد HTTP
return response, status_code
- السطر 16: تتطلب الدالة [json_response] معلمتين:
- [réponse]: القاموس الذي يجب إرسال السلسلة jSON منه إلى عميل الويب؛
- [status_code]: رمز حالة الرد HTTP؛
- السطر 18: يتم تحديد نص الرد jSON؛
- السطر 20: نضيف الرأس HTTP الذي يُعلم عميل الويب بأنه سيتلقى jSON؛
- السطر 22: يتم إرسال الرد HTTP إلى الكود المستدعي. ويقع على عاتقه إرساله إلى عميل الويب؛
يتطور الملف [__init__.py] على النحو التالي:
from .myutils import set_syspath, json_response
يتم تثبيت الإصدار الجديد من [myutils] ضمن الوحدات النمطية ذات النطاق الآلي باستخدام الأمر [pip install .] في محطة Pycharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
- السطر 1: يجب أن تكون في مجلد [packages] لكتابة هذه التعليمات؛
يستمر كود البرنامج النصي [server_01] على النحو التالي:
…
# هل توجد أخطاء؟
if erreurs:
# يتم إرسال استجابة خطأ إلى العميل
résultats = {"réponse": {"erreurs": erreurs}}
return json_response(résultats, status.HTTP_400_BAD_REQUEST)
# لا توجد أخطاء، يمكننا العمل
# حساب الضريبة
taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# يتم إرسال الرد إلى العميل
return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
- السطر 10: عند الوصول إلى هذا الموضع، تكون المعلمات المتوقعة في URL موجودة وصحيحة؛
- السطر 10: يتم إنشاء الكائن [TaxPayer] الذي يمثل المكلف؛
- السطر 11: يُطلب من الطبقة [métier] حساب الضريبة. تجدر الإشارة إلى أن العناصر التي تحسبها الطبقة [métier] يتم إدراجها في الكائن [taxpayer] الذي تم تمريره كمعلمة؛
- السطر 13: يتم إرسال الرد إلى عميل الويب في شكل سلسلة jSON. وهذه السلسلة هي السلسلة jSON الخاصة بقاموس. بالارتباط مع المفتاح [result]، يتم وضع قاموس الكائن [taxpayer] فيه. لم يكن بالإمكان إدراج الكائن [taxpayer] نفسه لأنه غير قابل للتسلسل إلى jSON؛
نقوم بإنشاء تكوينين للتنفيذ، أحدهما لـ MySQL، والآخر لـ PostgreSQL:

فيما يلي بعض أمثلة التنفيذ (قمت بتشغيل التطبيق [server_01] واستخدمت SGBD، ثم طلبت URL http://localhost:5000/ باستخدام متصفح):


فيما يلي مثال على التنفيذ في وحدة التحكم في Postman:

GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT
{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
- السطر 1: تم طلب URL غير صحيح؛
- السطر 10: يرد الخادم بالحالة 400 BAD REQUEST؛
23.2.2. الإصدار 2

يعزل الإصدار 2 من الخادم معالجة URL في الوحدة النمطية [index_controller] [5]:
# استيراد التبعيات
import re
from flask_api import status
from werkzeug.local import LocalProxy
# URL مع المعلمات التالية: /?متزوج=xx&أطفال=yy&راتب=zz
def execute(request: LocalProxy, config: dict) -> tuple:
# المعالون
from TaxPayer import TaxPayer
# في البداية لا توجد أخطاء
erreurs = []
# يجب أن تحتوي الاستعلام على ثلاثة معلمات
if len(request.args) != 3:
erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")
# يتم استرداد الحالة الاجتماعية لـ URL
marié = request.args.get('marié')
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
marié = marié.strip().lower()
erreur = marié != "oui" and marié != "non"
if erreur:
erreurs.append(f"paramétre marié [{marié}] invalide")
# يتم استرداد عدد أطفال URL
enfants = request.args.get('enfants')
if enfants is None:
erreurs.append("paramètre [enfants] manquant")
else:
enfants = enfants.strip()
match = re.match(r"^\d+", enfants)
if not match:
erreurs.append(f"paramétre enfants {enfants} invalide")
else:
enfants = int(enfants)
# يتم استرداد الراتب الخاص بـ URL
salaire = request.args.get('salaire')
if salaire is None:
erreurs.append("paramètre [salaire] manquant")
else:
salaire = salaire.strip()
match = re.match(r"^\d+", salaire)
if not match:
erreurs.append(f"paramétre salaire {salaire} invalide")
else:
salaire = int(salaire)
# هل توجد معلمات أخرى في URL؟
for key in request.args.keys():
if not key in ['marié', 'enfants', 'salaire']:
erreurs.append(f"paramètre [{key}] invalide")
# هل توجد أخطاء؟
if erreurs:
# يتم إرسال رد خطأ إلى العميل
résultats = {"réponse": {"erreurs": erreurs}}
return résultats, status.HTTP_400_BAD_REQUEST
# لا توجد أخطاء، يمكننا العمل
# حساب الضريبة
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
# يتم إرسال الرد إلى العميل
return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
- السطر 9: تتلقى الدالة [execute] معلمتين:
- [request]: طلب HTTP من العميل؛
- [config]: قاموس إعدادات التطبيق؛
النص البرمجي [server_02] هو كما يلي:
# في انتظار معلمة mysql أو pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# يتم تكوين التطبيق
import config
config = config.configure({'sgbd': sgbd})
# التبعيات
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller
# استرداد البيانات من مصلحة الضرائب
try:
# ستكون «admindata» بيانات على نطاق التطبيق للقراءة فقط
config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# تطبيق Flask
app = Flask(__name__)
# الصفحة الرئيسية URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
# يتم تنفيذ الاستعلام
résultat, statusCode = index_controller.execute(request, config)
# إرسال الاستجابة
return json_response(résultat, statusCode)
# اليد فقط
if __name__ == '__main__':
# يتم تشغيل الخادم
app.config.update(ENV="development", DEBUG=True)
app.run()
- السطور 36-41: معالجة العنوان /؛
- السطر 39: استخدام الدالة [IndexController.execute]؛
سنستخدم هذه التقنية من الآن فصاعدًا: سيتم معالجة كل مسار بواسطة وحدة نمطية خاصة به.
نتائج التنفيذ هي نفسها كما في الإصدار 1.
23.2.3. الإصدار 3

تقدم الإصدار 3 مفهوم المصادقة.
يصبح النص البرمجي [server_03] كما يلي:
# في انتظار معلمة mysql أو pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# يتم تكوين التطبيق
import config
config = config.configure({'sgbd': sgbd})
# التبعيات
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller
# استرداد البيانات من مصلحة الضرائب
try:
# سيكون config[‘admindata’] بيانات على مستوى التطبيق للقراءة فقط
config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# مدير المصادقة
auth = HTTPBasicAuth()
# طريقة المصادقة
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
# قائمة المستخدمين
users = config['users']
# يتم تصفح هذه القائمة
for user in users:
if user['login'] == login and user['password'] == password:
return True
# لم يتم العثور على
return False
# تطبيق Flask
app = Flask(__name__)
# الصفحة الرئيسية URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
# يتم تنفيذ الاستعلام
résultat, statusCode = index_controller.execute(request, config)
# يتم إرسال الرد
return json_response(résultat, statusCode)
# اليد فقط
if __name__ == '__main__':
# يتم تشغيل الخادم
app.config.update(ENV="development", DEBUG=True)
app.run()
- السطر 21: يتم استيراد مدير المصادقة. توجد أنواع مختلفة من المصادقة على خادم الويب. النوع الذي نستخدمه هنا يُسمى [HTTP Basic]. يتبع كل نوع من أنواع المصادقة حوارًا محددًا بين العميل والخادم؛
- السطر 33: يتم إنشاء مثيل لمدير المصادقة؛
- السطر 37: يقوم التعليق التوضيحي [@auth.verify_password] بتمييز الدالة التي يجب تنفيذها عندما يرغب مدير المصادقة في التحقق من اسم المستخدم وكلمة المرور المرسلة من العميل وفقًا لبروتوكول [HTTP Basic]؛
- السطر 55: يحدد التعليق التوضيحي [@auth.login_required] مسارًا يجب أن يتم مصادقة العميل عبر الويب عليه. إذا لم يكن العميل عبر الويب قد أرسل بيانات اعتماده بعد، فسيطلبها منه الخادم تلقائيًا وفقًا لبروتوكول HTTP الأساسي؛
يجب تثبيت الوحدة النمطية [flask_httpauth]:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.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->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.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->flask_httpauth) (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->flask_httpauth) (1.0.1)
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->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0
لنرى ما يحدث باستخدام وحدة التحكم Postman. عليك:
- تقوم بإنشاء إعداد للتشغيل؛
- تشغيل تطبيق الويب؛
- تشغيل SGBD الذي تختاره؛
- تطلب URL [/] باستخدام Postman؛
الحوار بين العميل والخادم في وحدة التحكم Postman هو كما يلي:
- السطر 10: يرد الخادم بأننا غير مخولين بالوصول إلى URL و [/]؛
- السطر 13: يُشير إلى بروتوكول المصادقة الذي يجب استخدامه، وهو في هذه الحالة بروتوكول المصادقة الأساسية (Auth Basic)؛
يمكن تكوين Postman بحيث يرسل بيانات اعتماد المستخدم وفقًا لبروتوكول «Auth Basic»:

- في [6-7] نضع بيانات اعتماد المستخدم الموجودة في البرنامج النصي [config]:
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
يصبح الحوار بين العميل والخادم في وحدة التحكم Postman كما يلي:
GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
- السطر 2: يرسل عميل Postman معرّفات المستخدم [admin / admin] في شكل مشفر؛
- السطر 17: يرد الخادم بشكل صحيح. ويشير إلى وجود أخطاء لأننا لم نرسل المعلمات [marié, enfants, salaire] (السطر 1) ولكنه لا يشير إلى أي خطأ في المصادقة؛
الآن لنطلب URL / باستخدام متصفح (Firefox أدناه):

- وكما هو الحال مع Postman، تلقى Firefox الرد HTTP من الخادم مع الرؤوس HTTP:
لا يوقف Firefox، شأنه شأن المتصفحات الأخرى، عملية الحوار عند تلقيه هذه الرؤوس. بل يطلب من المستخدم إدخال بيانات الاعتماد التي يطلبها الخادم. يكفي في المثال أعلاه كتابة admin / admin لتلقي الرد من الخادم:

23.3. عميل الويب الخاص بخادم حساب الضرائب
23.3.1. مقدمة
في الفقرة السابقة، كان عميل الويب لخادم حساب الضرائب عبارة عن متصفح. في هذا الجزء، سيكون عميل الويب عبارة عن برنامج نصي يعمل على وحدة التحكم. وتصبح البنية كما يلي:

- يتكون عميل الويب من الطبقات [1-2]؛
- يتكون خادم الويب من الطبقات [3-9]. وقد ورد في الفقرة السابقة؛
لذا يتعين علينا كتابة الطبقات [1-2].
يجب أن تتمكن الطبقة [dao] [2] من التواصل مع خادم الويب [3]. نحن نعرف الآن بروتوكول HTTP ويمكننا كتابة برنامج نصي، باستخدام الوحدة النمطية [pycurl] التي درسناها سابقًا على سبيل المثال، يتواصل مع خادم الويب [3]. ومع ذلك، توجد وحدات متخصصة في الحوارات بين العميل والخادم HTTP. سنستخدم إحداها، وهي الوحدة [requests]:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
|| 61 kB 137 kB/s
Collecting idna<3,>=2.5
Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
|| 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
|| 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
|| 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
|| 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9
هيكل ملفات البرامج النصية لعميل الويب هو كما يلي:

ستقوم البرنامج النصي بتنفيذ تطبيق حساب الضريبة في الوضع الدفعي الموصوف منذ |الإصدار 1|. أحدث إصدار من هذا التطبيق هو |الإصدار 5|. نذكر طريقة عمله:
- يتم تجميع دافعي الضرائب الذين سيتم حساب ضرائبهم في الملف النصي [taxpayersdata.txt]:
- تُحفظ النتائج في ملفين:
- يجمع الملف النصي [errors.txt] الأخطاء المكتشفة في ملف المكلفين:
Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt
Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
- (تابع)
- يجمع الملف jSON [résultats.json] نتائج حسابات الضرائب الخاصة بمختلف دافعي الضرائب:
[
{
"id": 0,
"marié": "oui",
"enfants": 2,
"salaire": 55555,
"impôt": 2814,
"surcôte": 0,
"taux": 0.14,
"décôte": 0,
"réduction": 0
},
{
"id": 1,
"marié": "oui",
"enfants": 2,
"salaire": 50000,
"impôt": 1384,
"surcôte": 0,
"taux": 0.14,
"décôte": 384,
"réduction": 347
},
…
]
23.3.2. إعدادات عميل الويب

يتم التكوين باستخدام نصين برمجيين:
- [config] الذي يتولى التهيئة الكاملة باستثناء طبقات البنية؛
- [config_layers] الذي يتولى تكوين طبقات البنية؛
النص البرمجي [config] هو كما يلي:
def configure(config: dict) -> dict:
import os
# الخطوة 1 ------
# مجلد هذا الملف
script_dir = os.path.dirname(os.path.abspath(__file__))
# المسار الجذري
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# التبعيات المطلقة
absolute_dependencies = [
# مجلدات المشروع
# BaseEntity، MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao، InterfaceImpôtsMétier، InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao، ImpôtsConsole، ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData، ImpôtsError، TaxPayer
f"{root_dir}/impots/v04/entities",
# الثوابت، الشرائح
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# نصوص التكوين
script_dir,
]
# تحديد مسار النظام
from myutils import set_syspath
set_syspath(absolute_dependencies)
# الخطوة 2 ------
# تكوين التطبيق باستخدام الثوابت
config.update({
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
}
}
)
# الخطوة 3 ------
# إنشاء مثيلات للطبقات
import config_layers
config['layers'] = config_layers.configure(config)
# تطبيق التكوين
return config
- السطر 1: تتلقى الدالة [configure] كمعلمة القاموس الذي يجب ملؤه بمعلومات التكوين. قد يكون هذا القاموس مملوءًا مسبقًا أو فارغًا. هنا، سيكون فارغًا؛
- الأسطر 40-42: الأسماء المطلقة للملفات النصية الثلاثة التي تديرها الطبقة [dao]؛
- الأسطر 43-50: مرتبطة بالمفتاح [server]، المعلومات التي يجب أن تعرفها الطبقة [dao] عن خادم الويب الذي يجب أن تتواصل معه:
- السطر 44: URL الخاص بخدمة الويب؛
- السطر 45: تكون قيمة المفتاح [authBasic] هي True إذا كان الوصول إلى URL يتطلب مصادقة من النوع Basic؛
- الأسطر 46-49: بيانات تعريف المستخدم الذي سيقوم بالمصادقة في حالة طلب المصادقة؛
- السطران 56-57: يتم إنشاء مثيلات للطبقات، وهنا الطبقة الوحيدة [dao]، ويتم وضع مراجع الطبقات في [config] المرتبطة بالمفتاح [layers]؛
النص البرمجي [config_layers] هو كما يلي:
def configure(config: dict) -> dict:
# إنشاء مثيلات طبقات التطبيق
# طبقة DAO
from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
dao = ImpôtsDaoWithHttpClient(config)
# إجراء تكوين الطبقات
return {
"dao": dao
}
- السطر 1: تستقبل الدالة [configure] القاموس الذي يحدد إعدادات التطبيق؛
- الأسطر 4-6: يتم إنشاء مثيل للطبقة [dao]. في السطر 6، يتم تمرير تكوين التطبيق إليها، حيث ستجد المعلومات التي تحتاجها؛
- الأسطر 8-11: يتم إرجاع قاموس تم إدراج مرجع الطبقة [dao] فيه؛
23.3.3. البرنامج النصي الرئيسي [main]
البرنامج النصي الرئيسي [main] هو نسخة معدلة من البرنامج النصي الخاص بـ |الإصدار 5|:
# يتم تكوين التطبيق
import config
config = config.configure({})
# التبعيات
from ImpôtsError import ImpôtsError
# الكود
try:
# استرداد الطبقة [dao]
dao = config["layers"]["dao"]
# قراءة بيانات دافعي الضرائب
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# المكلفين؟
if not taxpayers:
raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# حساب ضريبة دافعي الضرائب
for taxpayer in taxpayers:
# «المكلف» هو معلمة إدخال ومخرج في آن واحد
# سيتم تعديل «المكلف»
dao.calculate_tax(taxpayer)
# كتابة النتائج في ملف نصي
dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
# عرض الخطأ
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# انتهى
print("Travail terminé...")
- السطران 2-3: تم تكوين التطبيق؛
- السطر 13: توفر الطبقة [dao] قائمة بالمكلفين الذين يجب حساب ضرائبهم؛
- السطر 21: تقوم الطبقة [dao] بحساب الضريبة لكل منهم؛
- السطر 23: يتم تسجيل النتائج في ملف jSON؛
23.3.4. تنفيذ الطبقة [dao]

لنعد إلى بنية العميل/الخادم المستخدمة:

- في [2, 6]، نرى أن الطبقة [dao] لها دوران:
- فهي تصل إلى نظام الملفات لقراءة بيانات دافعي الضرائب وكتابة نتائج حسابات الضرائب في آن واحد. لدينا بالفعل فئة |AbstractImpôtsDao| قادرة على القيام بذلك. وقد تم استخدامها منذ |الإصدار 4|؛
- كما أنها تتواصل مع خادم الويب [3]؛
في |الإصدار 5|، كان البرنامج النصي الرئيسي [main] [1] يتواصل مباشرةً مع الطبقة [métier] [4]. نرغب في عدم تغيير هذا البرنامج النصي. ولذلك، سنعمل على أن تقوم الطبقة [dao] [2] بتنفيذ واجهة الطبقة [métier] [4]. وبذلك، سيبدو للبرنامج النصي الرئيسي [main] أنه يتواصل مباشرة مع الطبقة [métier] [4]، ويمكنه تجاهل تمامًا حقيقة أن هذه الطبقة موجودة على جهاز آخر.
قد يكون تعريف الفئة التي تنفذ الطبقة [dao] [2] كما يلي:
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
- الفئة [ImpôtsDaoWithHttpClient]:
- ترث الفئة [AbstractImpôtsDao]، مما سيسمح لها بإدارة التفاعل مع نظام الملفات [6]؛
- تنفذ الواجهة [InterfaceImpôtsMétier] لتجنب الحاجة إلى تغيير البرنامج النصي الرئيسي [main] الخاص بـ |الإصدار 5|؛
فيما يلي الكود الكامل لفئة [ImpôtsDaoWithHttpClient]:
# الاستيرادات
import requests
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# المنشئ
def __init__(self, config: dict):
# تهيئة الكائن الأصلي
AbstractImpôtsDao.__init__(self, config)
# تخزين المعلمات
self.__config_server = config["server"]
# طريقة غير مستخدمة من [AbstractImpôtsDao]
def get_admindata(self) -> AdminData:
pass
# حساب الضريبة
def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
# يُسمح بترحيل الاستثناءات
# معلمات get
params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
# تسجيل الدخول باستخدام المصادقة الأساسية (Auth Basic)؟
if self.__config_server['authBasic']:
response = requests.get(
# URL للخادم المستعلم عنه
self.__config_server['urlServer'],
# معلمات URL
params=params,
# المصادقة الأساسية
auth=(
self.__config_server["user"]["login"],
self.__config_server["user"]["password"]))
else:
# اتصال بدون مصادقة Auth Basic
response = requests.get(self.__config_server['urlServer'], params=params)
# التحقق
print(response.text)
# رمز حالة الرد HTTP
status_code = response.status_code
# يتم وضع الرد jSON في قاموس
résultat = response.json()
# خطأ إذا كان رمز الحالة مختلفًا عن 200 OK
if status_code != status.HTTP_200_OK:
# نعلم أن الأخطاء قد تم ربطها بالمفتاح [erreurs] الخاص بالاستجابة
raise ImpôtsError(87, résultat['réponse']['erreurs'])
# من المعروف أن النتيجة قد ارتبطت بالمفتاح [result] في الرد
# يتم تعديل معلمة الإدخال باستخدام هذه النتيجة
taxpayer.fromdict(résultat["réponse"]["result"])
- الأسطر 21-23: تحتوي الفئة [AbstractImpôtsDao] (السطر 12) على طريقة مجردة [get_admindata]. نحن مضطرون لتنفيذها حتى لو لم نستخدمها (يتم إدارة admindata بواسطة الخادم وليس بواسطة العميل)؛
- السطر 26: تنتمي الطريقة [calculate_tax] إلى الواجهة [InterfaceImpôtsMétier] (السطر 12). وعلينا تنفيذها؛
- السطر 15: يتلقى المنشئ معلمة واحدة هي قاموس إعدادات التطبيق؛
- السطران 16-17: يتم تهيئة الفئة الأم [AbstractImpôtsDao] عن طريق تمرير تكوين التطبيق إليها أيضًا. وستجد فيها أسماء الملفات النصية الثلاثة التي يتعين عليها إدارتها؛
- السطران 18-19: يتم تخزين المعلومات المتعلقة بخادم الويب الخاص بحساب الضريبة محليًّا داخل الفئة؛
- السطر 26: تتلقى الطريقة [calculate_tax] كمعلمة كائنًا من النوع |Taxpayer|. وللامتثال لتوقيع الطريقة [InterfaceImpôtsMétier.calculate_tax]، تتلقى أيضًا معلمة [admindata] التي من المفترض أن تغلف بيانات مصلحة الضرائب. أما على جانب العميل، فلا تتوفر هذه البيانات. وستظل هذه المعلمة دائمًا عند [None]. ويشير هذا التلاعب إلى أن الفئة [ImpôtsMétier] قد كُتبت بشكل خاطئ في البداية:
- كان من المفترض أن تكون توقيع [calculate_tax] ببساطة:
def calculate_tax(self, taxpayer: TaxPayer)
وكان ينبغي تمرير المعلمة [admindata : AdminData] إلى مُنشئ الفئة؛
- السطر 27: لم يتم تغليف كود الأسلوب [calculate_tax] في بنية try / catch / finally. وهذا يعني أن الاستثناءات المحتملة لن تتم معالجتها وستنتقل إلى الكود المستدعي، وهو في هذه الحالة البرنامج النصي [main]. وهذا البرنامج النصي يوقف بالفعل جميع الاستثناءات التي تنتقل من الطبقة [dao]؛
- السطر 28: يتم حساب الضريبة على جانب الخادم. لذا سيكون من الضروري التواصل معه. ويتم ذلك باستخدام الوحدة النمطية [requests] التي تم استيرادها في السطر 2؛
- الأسطر 31-43: لإرسال طلب GET إلى خادم الويب، نستخدم الطريقة [requests.get]:
- الأسطر 33-34: المعلمة الأولى للطريقة هي URL المراد الاتصال بها؛
- الأسطر 35-40: المعلمتان الأخريان هما معلمتان مسمايتان ولا يهم ترتيبهما؛
- السطران 35-36: يجب أن تكون قيمة المعلمة المسماة [params] عبارة عن قاموس يحتوي على المعلومات المطلوب إدراجها في URL بالصيغة [/url ?param1=valeur1¶m2=valeur2&…]؛
- السطر 29: القاموس الذي يحتوي على المعلمات الثلاثة [marié, enfants, salaire] التي ينتظرها خادم الويب. لا داعي للقلق بشأن الترميز (المسمى urlencoded) الذي يجب أن تخضع له هذه المعلمات. يتولى [requests] هذه المهمة؛
- الأسطر 37-40: المعلمة المسماة [auth] هي مجموعة مكونة من عنصرين (login، password). وهي تمثل بيانات اعتماد المصادقة من النوع Basic؛
- الأسطر 44-45: هذان السطران لهما غرض تعليمي فقط (سيتم وضعهما في تعليقات عند انتهاء عملية تصحيح الأخطاء):
- يمثل [response] الرد HTTP الوارد من الخادم؛
- [response.text] يمثل نص المستند المُغلف في هذه الاستجابة. في مرحلة التصحيح، من المفيد التحقق مما أرسله الخادم إلينا؛
- السطر 47: [response.status_code] هو رمز الحالة HTTP للاستجابة المستلمة. لا يرسل خادمنا سوى ثلاثة رموز:
- 200 OK
- 400 BAD REQUEST
- 500 INTERNAL SERVER ERROR
- السطر 49: يرسل خادمنا دائمًا jSON حتى في حالة حدوث خطأ. تقوم الدالة [response.json()] بإنشاء قاموس استنادًا إلى السلسلة jSON المستلمة. ولنتذكر الشكلين المحتملين للسلسلة jSON:
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
- الأسطر 51-53: إذا لم يكن رمز الحالة هو 200، يتم إثارة استثناء مع تضمين رسائل الخطأ في الرد؛
- السطر 56: يتم استرداد القاموس الناتج عن حساب الضريبة واستخدامه لتحديث معلمة الإدخال [taxpayer]؛
23.3.5. التنفيذ
لتشغيل العميل:
- قم بتشغيل الخادم [server_03] مع SGBD الذي تختاره؛
- قم بتنفيذ البرنامج النصي [main] الخاص بالعميل؛
ستجد النتائج في المجلد [data/output]. وهي نفس النتائج الخاصة بالإصدار 5.
23.4. اختبارات الطبقة [dao]
لنعد إلى بنية تطبيق العميل/الخادم:
- في العميل المكتوب، تمكنا من جعل الطبقة [dao] [1] توفر نفس واجهة الطبقة [métier] [3]. لذلك سنستخدم في [4] فئة الاختبار |TestDaoMétier| التي سبق دراستها لاختبار الطبقة [métier] [3]؛
سيتم تنفيذ فئة الاختبار في البيئة التالية:

- التكوين [2] مطابق للتكوين [1] الذي درسناه للتو؛
فئة الاختبار [TestHttpClientDao] هي كما يلي:
import unittest
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'متزوج': 'نعم', 'أطفال': 2, 'راتب': 55555,
# 'الضريبة': 2814, 'الزيادة': 0, 'الخصم': 0, 'التخفيض': 0, 'المعدل': 0.14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# التحقق
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
def test_11(self) -> None:
from TaxPayer import TaxPayer
# {'متزوج': 'نعم', 'الأطفال': 3, 'الراتب': 200000,
# 'الضريبة': 42842, 'الزيادة': 17283, 'الخصم': 0, 'التخفيض': 0, 'المعدل': 0.41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
dao.calculate_tax(taxpayer)
# التحققات
self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
if __name__ == '__main__':
# يتم تكوين التطبيق
import config
config = config.configure({})
# طبقة DAO
dao = config['layers']['dao']
# تنفيذ أساليب الاختبار
print("tests en cours...")
unittest.main()
هذه الفئة مشابهة لتلك التي تمت دراستها سابقًا في الإصدار 4 من التطبيق.
- السطران 40-41: يتم تكوين بيئة الاختبارات؛
- السطر 44: يتم الحصول على مرجع للطبقة [dao]؛
- السطران 47-48: يتم تنفيذ الاختبارات؛
لتنفيذ الاختبارات، يتم إنشاء |تكوين تنفيذ|:

- يتم إنشاء تكوين تشغيل لبرنامج نصي في وحدة التحكم وليس لاختبار UnitTest؛
عند تنفيذ هذا التكوين، نحصل على النتائج التالية:
نجحت الاختبارات الـ 11.