25. تمرين تطبيقي: الإصدار 8
25.1. مقدمة
سنقوم بكتابة تطبيق عميل/خادم جديد. الجديد في الخادم هو أنه سيدير جلسة عمل. بدلاً من وضع بيانات إدارة الضرائب في كائن ذي نطاق [application]، سنضعها في كائن ذي نطاق [session]. وبذلك، تتراجع أداء الكود. عندما يمكن مشاركة كائن ما للقراءة فقط من قبل جميع المستخدمين، يُفضل جعله كائنًا بنطاق [application] بدلاً من نطاق [session]. ونكسب على الأقل في عرض النطاق الترددي لأننا نخفض بذلك حجم ملف تعريف الارتباط الخاص بالجلسة. لكننا نريد عرض تطبيق عميل/خادم حيث يتبادل العميل والخادم ملف تعريف ارتباط خاص بالجلسة.
لا تتغير بنية التطبيق:

25.2. خادم الويب
هيكل ملفات البرامج النصية للخادم هو كما يلي:

يتم الحصول على المجلد [http-servers/03] في البداية عن طريق نسخ المجلد [http-servers/02]. ثم يتم إجراء التعديلات.
25.2.1. التكوين
هي نفسها كما في |الإصدار السابق| مع بعض التعديلات في البرنامج النصي [config]:
# التبعيات المطلقة
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",
# index_controller
f"{script_dir}/../controllers",
# نصوص برمجية [config_database, config_layers]
script_dir,
# Logger، SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
]
- السطر 17: سنقوم بإعادة كتابة وحدة تحكم للوظيفة [index] التي تعالج الملف URL / ؛
- السطر 21: نستخدم الأدوات المساعدة من |الإصدار السابق|؛
25.2.2. البرنامج النصي الرئيسي [main]
يُحدث البرنامج النصي الجديد [main] بعض التعديلات على البرنامج النصي الرئيسي [main] من الإصدار السابق:
# يمكن تشغيل تطبيق Flask
app = Flask(__name__)
# المفتاح السري للجلسة
app.secret_key = os.urandom(12).hex()
- السطر 4: يتم إنشاء مفتاح سري للتطبيق. ومن المعروف أن هذا المفتاح ضروري لإدارة الجلسات؛
بعد ذلك، لم تعد البيانات الضريبية مطلوبة في كود [main]. تم حذف الأسطر التالية:
# استرداد بيانات مصلحة الضرائب
erreur = False
try:
# ستكون «admindata» بيانات ذات نطاق التطبيق وقابلة للقراءة فقط
config["admindata"] = config["layers"]["dao"].get_admindata()
# سجل النجاح
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# تسجيل الخطأ
erreur = True
# سجل الأخطاء
log = f"L'erreur suivante s'est produite : {ex}"
# وحدة التحكم
print(log)
# ملف السجلات
logger.write(f"{log}\n")
# رسالة بريد إلكتروني إلى المسؤول
send_adminmail(config, log)
بالإضافة إلى ذلك، يقبل المراقب [index_controller] معلمة إضافية، وهي جلسة Flask:
from flask import request, Flask, session
….
# يتم تنفيذ الطلب بواسطة وحدة تحكم
résultat, status_code = index_controller.execute(request, session, config)
25.2.3. وحدة التحكم [index_controller]
تدير وحدة التحكم [index_controller] الآن جلسة عمل:
# استيراد التبعيات
import os
import re
import threading
from flask_api import status
from werkzeug.local import LocalProxy
# URL مع التعيينات: /?متزوج=xx&أطفال=yy&راتب=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError
def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
# التبعيات
from TaxPayer import TaxPayer
# في البداية لا توجد أخطاء
erreurs = []
…
# أخطاء؟
if erreurs:
# يتم إرسال رد خطأ إلى العميل
return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST
# لا توجد أخطاء، يمكن العمل
# يتم استرداد التكوين المرتبط بالخيط
thread_name = threading.current_thread().name
logger = config[thread_name]["config"]["logger"]
# يتم تنفيذ الاستعلام
réponse = None
try:
# الحالة الأبسط - admindata مسجل الدخول بالفعل
if session.get('client_id') is not None:
# يتم استرداد معلومات الجلسة
client_id = session.get('client_id')
admindata = AdminData().fromdict(session.get('admindata'))
# السجل
logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
else:
# استرداد بيانات إدارة الضرائب
admindata = config["layers"]["dao"].get_admindata()
# تسجيل دخول admindata
session['admindata'] = admindata.asdict()
# يتم تخصيص رقم للعميل وإدراجه في الجلسة
# سيسمح لنا ذلك بتتبعه في سجلات الخادم
client_id = os.urandom(12).hex()
session['client_id'] = client_id
# سجل
logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
# حساب الضريبة
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# نرسل الرد إلى العميل
return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
except (BaseException, ImpôtsError) as erreur:
# إرسال الرد إلى العميل
return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- السطر 14: يتلقى المُراقب الجلسة الحالية من عميل الويب؛
- الأسطر 35-38: إذا كان لدى العميل جلسة عمل، فإنها تحتوي على مفتاحين:
- [client_id]: رقم العميل (السطر 37)؛
- [admindata]: بيانات مصلحة الضرائب في شكل قاموس (السطر 38)؛
- السطر 35: يتم التحقق مما إذا كانت الجلسة تحتوي على أحد المفتاحين المتوقعين؛
- الأسطر 42-51: في حالة عدم تهيئة جلسة عمل العميل بعد؛
- السطر 43: يتم استرداد بيانات مصلحة الضرائب من الطبقة [dao]؛
- السطر 45: يتم وضع هذه البيانات في الجلسة في شكل قاموس؛
- السطر 48: يتم تخصيص رقم عشوائي للعميل. وسيكون هذا الرقم مختلفًا لكل عميل؛
- السطر 49: يتم تخزين هذا الرقم في الجلسة؛
- السطر 51: يتم تسجيل حقيقة أن بيانات مصلحة الضرائب قد تم الحصول عليها من الطبقة [dao]. عادةً ما تكون عمليات الوصول إلى الطبقة [dao] مكلفة. ولهذا السبب يجب الحد منها. الفكرة هنا هي الحصول على البيانات الضريبية مرة واحدة من الطبقة [dao]، وتخزينها في الجلسة، ثم استرجاعها من هناك عند الطلبات اللاحقة من نفس العميل. تجدر الإشارة إلى أن هذا ليس الحل الأمثل. ونظرًا لأن البيانات الضريبية للسلطة الضريبية هي نفسها لجميع العملاء، فإن مكانها هو في كائن ذي نطاق تطبيق؛
- الأسطر 35-40: الحالة التي تم فيها تهيئة جلسة عمل العميل خلال طلب سابق؛
- السطر 37: يتم استرداد رقم العميل من الجلسة؛
- السطر 38: يتم استرداد البيانات الضريبية الخاصة بالإدارة من الجلسة؛
- السطر 40: يتم تسجيل حقيقة أن العميل قد حصل على البيانات الضريبية الخاصة بالإدارة في الجلسة؛
25.3. العميل على الويب

25.3.1. الطبقة [dao]
25.3.1.1. الفئة [ImpôtsDaoWithHttpSession]
يتم تنفيذ الطبقة [dao] بواسطة الفئة [ImpôtsDaoWithHttpSession] التالية:
# الاستيرادات
import requests
from flask_api import status
from myutils import decode_flask_session
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ôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):
# المنشئ
def __init__(self, config: dict):
# تهيئة العنصر الأصلي
AbstractImpôtsDao.__init__(self, config)
# تخزين عناصر التكوين
# التكوين العام
self.__config = config
# الخادم
self.__config_server = config["server"]
# وضع التصحيح
self.__debug = config["debug"]
# مسجل
self.__logger = None
# ملفات تعريف الارتباط
self.__cookies = None
# طريقة غير مستخدمة
def get_admindata(self) -> AdminData:
pass
# حساب الضريبة
def calculate_tax(self, 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"]),
cookies=self.__cookies)
else:
# تسجيل الدخول بدون مصادقة Auth Basic
response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
# يتم استرداد ملفات تعريف الارتباط من الرد إن وجدت
if response.cookies:
self.__cookies = response.cookies
# يتم استرداد ملف تعريف الارتباط الخاص بالجلسة
session_cookie = response.cookies.get('session')
# يتم فك تشفيره لتسجيل الدخول
if session_cookie:
# أداة تسجيل الدخول
if not self.__logger:
self.__logger = self.__config['logger']
# يتم تسجيل الدخول
self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")
# وضع التصحيح؟
if self.__debug:
# مسجل
if not self.__logger:
self.__logger = self.__config['logger']
# يتم التسجيل
self.__logger.write(f"{response.text}\n")
# رمز حالة الاستجابة 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"])
- السطر 30: ستتولى الطبقة [dao] إدارة قاموس ملفات تعريف الارتباط؛
- السطر 58: الخاصية [response.cookies] هي قاموس يجمع ملفات تعريف الارتباط المرسلة من الخادم في الرؤوس HTTP و[Set-Cookie]؛
- السطر 59: يتم تخزين ملفات تعريف الارتباط هذه في الطبقة [dao]. وسيتم إرسالها إلى الخادم عند الطلبات اللاحقة من نفس العميل؛
- الأسطر 60-68: على الرغم من أن ذلك ليس ضروريًا، إلا أننا نسترد ملف تعريف الارتباط الخاص بالجلسة. في قاموس ملفات تعريف الارتباط المرسلة من الخادم، يرتبط ملف تعريف الارتباط الخاص بالجلسة بالمفتاح [session]؛
- الأسطر 62-68: نقوم بفك تشفير ملف تعريف الارتباط الخاص بالجلسة لتسجيل الدخول؛
- السطر 68: سنعود لاحقًا إلى الدالة [decode_flask_session] التي تقوم بفك تشفير ملف تعريف الارتباط الخاص بالجلسة؛
- السطران 52 و57: مع كل طلب من نفس العميل، يتم إعادة إرسال ملفات تعريف الارتباط التي أرسلها الخادم إليه. وبهذه الطريقة يتم الحفاظ على جلسة عمل Flask بين العميل والخادم؛
يجب أن نتذكر الآن أن طبقة [dao] سيتم تنفيذها في وقت واحد بواسطة عدة خيوط. لذا، يجب فحص جميع خصائص مثيل الفئة والتأكد مما إذا كان الوصول المتزامن إلى هذه الخصائص يمثل مشكلة. هنا أضفنا الخاصية [self.__cookies]، في السطر 30. يتم تعديل هذه الخاصية في السطر 59. وبالتالي، لدينا وصول للكتابة إلى بيانات مشتركة بين جميع الخيوط. لكن هذا الوصول يمثل مشكلة: فكل خيط يمثل عميلاً معيناً له ملف تعريف ارتباط جلسة خاص به. ففي الواقع، تحتوي ملفات تعريف الارتباط هذه على رقم عميل (=خيط) فريد لكل عميل. وإذا لم نتخذ أي إجراء، فقد يقوم الخيط T2 بالكتابة فوق ملفات تعريف الارتباط الخاصة بالخيط T1.
لقد رأينا بالفعل طريقة لإدارة هذه المشكلة: يمكننا إنشاء مفاتيح مختلفة لكل مؤشر ترابط في الملف [config] الذي يتم تمريره كمعلمة إلى المنشئ (السطر 17). يمكننا، على سبيل المثال، استخدام اسم مؤشر الترابط كمفتاح:
- في السطر 59، يمكن كتابة:
config[thread_name][‘cookies’]=cookies
- في السطر 52، يمكننا عندئذٍ كتابة:
cookies=config[thread_name][‘cookies’]
سنستخدم هنا تقنية مختلفة: سيكون لكل مؤشر ترابط (=عميل) طبقة [dao] خاصة به. وبذلك، لن يمثل السطر 59 مشكلة بعد الآن لأن ملفات تعريف الارتباط المستخدمة ستكون خاصة بعميل واحد فقط.
لهذا الغرض، سنقوم بإنشاء فئة جديدة [ImpôtsDaoWithHttpSessionFactory].
25.3.1.2. وظيفة فك تشفير جلسة Flask
يتم تعريف الدالة [decode_flask_session] في البرنامج النصي [myutils]:

لقد درسنا بالفعل البرنامج النصي |myutils|. هذا البرنامج النصي هو وحدة نمطية على مستوى الجهاز يمكن للبرامج النصية المختلفة في هذه الدورة استيرادها باستخدام الأمر:
يتم فيها تعريف الدالة [decode_flask_session] على النحو التالي:
def decode_flask_session(cookie: str) -> str:
# المصدر: https://www.kirsle.net/wizards/flask-session.cgi
compressed = False
payload = cookie
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data.decode("utf-8")
- السطر 2: URL حيث عثرت على هذه الوظيفة؛
- السطر 1: المعلمة [cookie] هي سلسلة الأحرف المرتبطة بالمفتاح [session] في قاموس ملفات تعريف الارتباط التي يتلقاها عميل الويب؛
- الأسطر 3-16: لن أعلق على هذا الكود الذي لا أفهمه جيدًا؛
نضيف استيرادًا جديدًا في الملف [__init__.py]:
from .myutils import set_syspath, json_response, decode_flask_session
يتم تثبيت الإصدار الجديد من [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] لكتابة هذه التعليمات؛
25.3.1.3. الفئة [ImpôtsDaoWithHttpSessionFactory]
الفئة [ImpôtsDaoWithHttpSessionFactory] هي كما يلي:
from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession
class ImpôtsDaoWithHttpSessionFactory:
def __init__(self, config: dict):
# يتم تخزين المعلمة
self.__config = config
def new_instance(self):
# يتم إنشاء مثيل لطبقة [dao]
return ImpôtsDaoWithHttpSession(self.__config)
- تسمح الفئة [ImpôtsDaoWithHttpSessionFactory] بإنشاء تطبيق جديد للطبقة [dao] باستخدام الطريقة [new_instance] الواردة في الأسطر 10-12؛
25.3.2. التكوين
يتم تعديل البرنامج النصي [config_layers] الذي يقوم بتكوين طبقات عميل الويب على النحو التالي:
def configure(config: dict) -> dict:
# إنشاء مثيلات لطبقات التطبيق
# طبقة DAO
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# يتم تكوين الطبقات
return {
"dao_factory": dao_factory
}
- الأسطر 5-6: بدلاً من إنشاء مثيل لطبقة [dao] واحدة كما كان يتم سابقًا، يتم إنشاء مثيل لـ«مصنع» لهذه الطبقة (المصنع = وحدة إنتاج الكائنات، وهنا الطبقة [dao])؛
- الأسطر 9-11: يتم إرجاع تكوين الطبقات؛
25.3.3. البرنامج النصي الرئيسي للعميل
يتطور البرنامج النصي [main] على النحو التالي مقارنةً بالإصدار السابق:
# يتم تكوين التطبيق
import config
config = config.configure({})
# التبعيات
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# تنفيذ الطبقة [dao] في مؤشر ترابط
# "taxpayers" هي قائمة بالمكلفين
def thread_function(thread_dao, logger, taxpayers: list):
…
# قائمة مؤشرات الترابط الخاصة بالعميل
threads = []
logger = None
# الكود
try:
…
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
…
# يجب أن يكون لكل مؤشر ترابط طبقة خاصة به [dao] لإدارة ملف تعريف ارتباط الجلسة الخاص به بشكل صحيح
thread_dao = dao_factory.new_instance()
# يتم إنشاء الخيط
thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
# يتم إضافته إلى قائمة مؤشرات الترابط الخاصة بالنص البرمجي الرئيسي
threads.append(thread)
# يتم تشغيل الخيط - هذه العملية غير متزامنة - ولا يتم انتظار نتيجة الخيط
thread.start()
# يانتظر الخيط الرئيسي انتهاء جميع الخيوط التي أطلقها
…
except BaseException as erreur:
# عرض الخطأ
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# يتم إغلاق أداة التسجيل
if logger:
logger.close()
# انتهينا
print("Travail terminé...")
# انتهت الخيوط التي قد تكون لا تزال موجودة في حالة التوقف بسبب خطأ
sys.exit()
- السطور 29-30: لكل مؤشر ترابط طبقته الخاصة [dao]؛
25.3.4. تشغيل العميل
يتم تشغيل خادم الويب، ثم يتم تشغيل البرنامج النصي SGBD، ثم يتم تشغيل خادم البريد الإلكتروني [hMailServer]. بعد ذلك، يتم تشغيل البرنامج النصي [main] الخاص بالعميل على الويب. وتكون سجلات التنفيذ في [data/logs/logs.txt] كما يلي:
2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.537161, Thread-6 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
- يوجد إجمالًا 6 خيوط، أي 6 عملاء (الأسطر 1 و3 و4 و6 و8 و9) يستعلمون في وقت واحد عن خادم حساب الضريبة؛
- سنقوم بمتابعة الخيط [Thread-4] الذي يدير 3 دافعي ضرائب (السطر 6). وسيقوم هذا الخيط بإرسال ثلاث طلبات متتالية إلى خادم حساب الضريبة؛
- السطر 10: الطلب الأول لـ [Thread-4]؛
- السطر 13: تلقى [Thread-4] الرد على طلبه الأول. ويجد فيه ملف تعريف ارتباط للجلسة يحتوي على الرقم [fa3c83b82761c83e13217967] الذي خصصه له الخادم؛
- السطر 14: ضريبة المكلف الأول؛
- السطر 16: يقوم [Thread-4] بتقديم طلب بشأن المكلف الثاني؛
- السطر 43: [Thread-4] يتلقى ضريبة المكلف الثاني؛
- السطر 45: يقوم [Thread-4] بتقديم طلب بشأن المكلف الثالث؛
- السطر 49: [Thread-4] يستلم الضريبة من المكلف الثالث؛
- السطر 51: [Thread-4] أنهى عمله؛
الآن، لنلقِ نظرة على كيفية معالجة الطلبات الثلاثة الخاصة بـ [Thread-4] على جانب الخادم. سنتمكن من تتبعه في سجلات الخادم بفضل رقم العميل الخاص به [fa3c83b82761c83e13217967].
سجلات [data/logs/logs.txt] على جانب الخادم هي كما يلي:
2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=نعم&أطفال=2&الراتب=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=لا&أطفال=0&الراتب=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=نعم&أطفال=2&الراتب=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=لا&أطفال=3&الراتب=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=نعم&أطفال=3&الراتب=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=نعم&أطفال=3&الراتب=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=لا&أطفال=2&الراتب=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=نعم&أطفال=2&الراتب=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=لا&أطفال=0&الراتب=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?متزوج=نعم&أطفال=5&الراتب=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
- يظهر العميل [fa3c83b82761c83e13217967] لأول مرة في السطر 14: لحساب الضريبة، اضطر الخادم إلى استرجاع بيانات مصلحة الضرائب من قاعدة البيانات؛
- ثم نجد العميل [fa3c83b82761c83e13217967] في السطر 36. هذه المرة، يعثر الخادم على بيانات مصلحة الضرائب في الجلسة الحالية، مما يوفر عليه الوصول — الذي قد يكون مكلفًا — إلى الطبقة [dao]؛
- نجد العميل [fa3c83b82761c83e13217967] للمرة الثالثة في السطر 42، حيث يستخدم الخادم مرة أخرى جلسة عمل العميل؛
يوضح هذا المثال جيدًا فائدة الجلسة بالنسبة للعميل: حيث يتم تخزين البيانات المشتركة بين جميع طلبات هذا العميل والتي يكون الحصول عليها مكلفًا.
من جانب العميل، تكون النتائج في الملف [data/output/résultats.json] هي نفسها كما في الإصدارات السابقة.
25.4. اختبارات الطبقة [dao]
كما فعلنا في |الإصدارات السابقة|، نقوم باختبار طبقة [dao] الخاصة بالعميل:

سيتم تنفيذ فئة الاختبار في البيئة التالية:

- التكوين [2] مطابق للتكوين [1] الذي درسناه للتو؛
فئة الاختبار [TestHttpClientDao] هي كما يلي:
import unittest
from Logger import Logger
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)
…
if __name__ == '__main__':
# يتم تكوين التطبيق
import config
config = config.configure({})
# مسجل
logger = Logger(config["logsFilename"])
# يتم تخزينه في ملف التكوين
config["logger"] = logger
# استرداد مصنع الطبقة [dao]
dao_factory = config["layers"]["dao_factory"]
# يتم إنشاء مثيل للطبقة [dao]
dao = dao_factory.new_instance()
# يتم تنفيذ طرق الاختبار
print("tests en cours...")
unittest.main()
- نقوم بإنشاء |تكوين تشغيل| لهذا الاختبار؛
- نقوم بتشغيل خادم الويب مع بيئته بالكامل؛
- نُجري الاختبار؛
وكانت النتائج كما يلي:
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/impots/http-clients/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
OK
Process finished with exit code 0