28. تمرين تطبيقي: الإصدار 10
28.1. مقدمة
في أمثلة عملاء خادم حساب الضرائب، كانت الخيوط ترسل N طلبات بالتسلسل إذا كان عليها معالجة N دافعي ضرائب. الفكرة هنا هي إرسال طلب واحد يشمل N دافعي الضرائب. لكل واحد منهم، يجب إرسال المعلومات [marié, enfants, salaire]. يمكن إرسالها كمعلمات:
- من URL. وبذلك نحصل على URL طويل وغير ذي أهمية كبيرة؛
- في نص (body) الطلب HTTP. ومن المعروف أن هذا النص مخفي عن المستخدم الذي يستخدم متصفحًا؛
في كلتا الحالتين، يمكننا استخدام طلب [GET] أو [POST]. سنستخدم طلب POST مع المعلمات المُغلفة في نص الطلب HTTP.
لم تتغير بنية العميل/الخادم:

28.2. خادم الويب

يتم الحصول على المجلد [http-servers/05] في البداية عن طريق نسخ المجلد [http-servers/02]. نعود إلى التبادلات jSON بين العميل والخادم. وقد رأينا أن الانتقال من jSON إلى XML أمر بسيط للغاية.
28.2.1. التكوين
تظل تهيئة ملف [config, config_database, config_layers] مماثلة لتلك الموجودة في الإصدارات السابقة. ولن نعود إلى هذا الموضوع.
28.2.2. البرنامج النصي الرئيسي [main]
البرنامج النصي [main] مطابق للبرنامج النصي الموجود في المجلد [http-servers/02] الذي قمنا بنسخه. هناك اختلاف واحد فقط:
# الصفحة الرئيسية URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():
…
- السطر 2: يُحصل الآن على URL / عبر POST؛
28.2.3. وحدة التحكم [index_controller]
يتطور عنصر التحكم [index_controller] على النحو التالي:
# استيراد المكتبات
import json
from flask_api import status
from werkzeug.local import LocalProxy
def execute(request: LocalProxy, config: dict) -> tuple:
# التبعيات
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
# استرداد نص المنشور - انتظار قائمة القواميس
msg_erreur = None
list_dict_taxpayers = None
# النص الأساسي jSON لـ POST
request_text = request.data
try:
# الذي يتم تحويله إلى قائمة من القواميس
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# يتم تسجيل الخطأ
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# هل لدينا قائمة غير فارغة؟
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# يتم تسجيل الخطأ
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# هل لدينا قائمة من القواميس؟
if not msg_erreur:
erreur = False
i = 0
while not erreur and i < len(list_dict_taxpayers):
erreur = not isinstance(list_dict_taxpayers[i], dict)
i += 1
# خطأ؟
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# هل هناك خطأ؟
if msg_erreur:
# يتم إرسال رد خطأ إلى العميل
résultats = {"réponse": {"erreurs": [msg_erreur]}}
return résultats, status.HTTP_400_BAD_REQUEST
# يتم التحقق من TaxPayers واحدًا تلو الآخر
# في البداية لا توجد أخطاء
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# يتم إنشاء TaxPayer من dict_taxpayer
msg_erreur = None
try:
# ستقوم العملية التالية باستبعاد الحالات التي لا تكون فيها المعلمات
# خصائص الفئة TaxPayer وكذلك الحالات التي تكون فيها قيمها
# غير صحيحة
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# يجب أن تكون بعض المفاتيح موجودة في القاموس
if not msg_erreur:
# يجب أن تكون المفاتيح [marié, enfants, salaire] موجودة في القاموس
keys = dict_taxpayer.keys()
if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
# هل توجد أخطاء؟
if msg_erreur:
# يُلاحظ وجود خطأ في TaxPayer نفسه
dict_taxpayer['erreur'] = msg_erreur
# يتم إضافة TaxPayer إلى قائمة الأخطاء
list_erreurs.append(dict_taxpayer)
# تمت معالجة جميع دافعي الضرائب - هل توجد أخطاء؟
if list_erreurs:
# يتم إرسال رد خطأ إلى العميل
résultats = {"réponse": {"erreurs": list_erreurs}}
return résultats, status.HTTP_400_BAD_REQUEST
# لا توجد أخطاء، يمكن متابعة العمل
# استرداد البيانات من مصلحة الضرائب
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# تتم معالجة TaxPayer واحدًا تلو الآخر
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# حساب الضريبة
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
'الراتب': dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# يتم تخزين النتيجة كقاموس
list_taxpayers.append(taxpayer.asdict())
# إرسال الرد إلى العميل
return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
except ImpôtsError as erreur:
# يتم إرسال رد خطأ إلى العميل
return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- السطر 9: يتلقى جهاز التحكم:
- الطلب [request] من العميل؛
- التكوين [config] من الخادم؛
- الأسطر 14-18: يتم استرداد نص الطلب POST. يمكن ترميز المعلمات المُغلفة في نص الطلب HTTP بطرق مختلفة. وقد سبق أن صادفنا إحدى هذه الطرق: [x-www-form-urlencoded]. سنستخدم هنا ترميزًا آخر: jSON؛
- السطر 18: يتيح [request.data] استرداد نص (body) الطلب HTTP. نسترد هنا نصًا، ونعلم أن هذا النص مأخوذ من jSON الذي يمثل قائمة القاموس [marié, enfants, salaire]؛
- الأسطر 19-24: يتم استرداد قائمة القواميس هذه؛
- الأسطر 22-24: إذا فشل استرداد jSON، يتم تسجيل الخطأ؛
- الأسطر 26-28: إذا تبين أن الكائن الذي تم استرداده ليس قائمة أو أنه قائمة فارغة، يتم تسجيل الخطأ؛
- الأسطر 29-38: إذا تم استرداد قائمة بنجاح، نتحقق من أنها قائمة قواميس بالفعل؛
- الأسطر 40-43: إذا حدث خطأ، نتوقف عند هذا الحد ونرسل رد خطأ إلى العميل؛
- الأسطر 45-69: نتحقق الآن من كل قاموس على حدة:
- يجب أن تحتوي على المفاتيح [marié, enfants, salaire]؛
- يجب أن تسمح بإنشاء كائن [TaxPayer] صالح؛
- الأسطر 65-69: إذا تم اكتشاف خطأ في أحد القواميس، يتم إدراجه في نفس القاموس المرتبط بالمفتاح «erreur»؛
- الأسطر 72-75: تم تجميع القواميس التي تحتوي على أخطاء في القائمة [list_erreurs]. إذا لم تكن هذه القائمة فارغة، يتم إرسالها في استجابة خطأ موجهة إلى العميل؛
- السطر 77: عند الوصول إلى هذه المرحلة، نعلم أنه يمكننا إنشاء قائمة من الكائنات من النوع [TaxPayer] استنادًا إلى نص الطلب المرسل من العميل؛
- الأسطر 84-91: نستفيد من قائمة القواميس المستلمة؛
- السطر 86: من أحد القواميس، نقوم بإنشاء كائن من النوع [TaxPayer]؛
- السطر 89: يتم حساب الضريبة الخاصة بهذا الكائن [TaxPayer]؛
- السطر 91: نعلم أن كائن [taxpayer] قد تم تعديله نتيجة لحساب الضريبة. يتم تحويله إلى قاموس وإضافته إلى قائمة النتائج؛
- السطر 93: يتم إرسال قائمة النتائج هذه إلى العميل؛
28.2.4. اختبارات الخادم
سنقوم باختبار الخادم باستخدام عميل Postman:
- نقوم بتشغيل خادم الويب، SGBD، وخادم البريد الإلكتروني [hMailServer]؛
- نقوم بتشغيل عميل Postman بالإضافة إلى وحدة التحكم الخاصة به (Ctrl-Alt-C)؛

- في [1]: نرسل طلبًا [POST]؛
- في [2]: URL الخاص بالخادم؛
- في [3]: نص الطلب HTTP؛
- إلى [5]: يُشار إلى أن هذا النص يجب إرساله في شكل سلسلة jSON؛
- في [4]: يتم الانتقال إلى الوضع [raw] للتمكن من نسخ/لصق سلسلة jSON؛
- في [6]: يتم لصق السلسلة jSON المأخوذة من أحد ملفات [résultats.json] الخاصة بالإصدارات المختلفة. ثم يتم الاحتفاظ لكل دافع ضرائب بالخصائص [marié, salaire, enfants] فقط؛

- في [7]، ننظر إلى الرؤوس HTTP التي سيرسلها عميل Postman إلى الخادم؛
- في [8]، نلاحظ أنه سيرسل إليه رأسًا [Content-Type] يُشير إلى أن الطلب يحتوي على نص مشفر بـ jSON. ويرجع ذلك إلى الاختيار [5] الذي تم اتخاذه سابقًا؛

- في [9-12]: نضع في الطلب المعرّفات التي يتوقعها الخادم؛
نرسل هذا الطلب. وتكون استجابة الخادم كما يلي:

- في [3]، تلقينا من jSON؛
- في [4]، ضريبة دافعي الضرائب؛
دعونا نستعرض في وحدة التحكم Postman (Ctrl-Alt-C) الحوار الذي دار بين العميل والخادم:
أرسل عميل Postman النص التالي:
- السطر 1: POST إلى الخادم؛
- السطر 2: رأس المصادقة HTTP؛
- السطر 3: يُعلم العميل الخادم بأنه يرسل إليه سلسلة jSON وأن حجم هذه السلسلة يبلغ 824 بايت (السطر 11)؛
- الأسطر 13-69: نص الطلب jSON؛
أرسل له الخادم النص التالي:
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:16:34 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
- السطر 1: نجحت الطلب؛
- السطر 2: نص استجابة الخادم عبارة عن سلسلة jSON. يبلغ حجمها 1461 بايت (السطر 3)؛
- السطر 7: رد الخادم jSON؛
لنختبر الآن حالات الخطأ.
الحالة 1: نرسل أي شيء
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3
abc
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
- السطر 13: تم إرسال السلسلة [abc] التي ليست سلسلة jSON صالحة (السطر 3)؛
- السطر 15: يرد الخادم برمز خطأ 400؛
- السطر 21: الرد jSON من الخادم؛
الحالة 2: لنرسل سلسلة صالحة هي jSON، وهي ليست قائمة
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17
{"att1":"value1"}
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}
الحالة 3: لنرسل سلسلة jSON تكون قائمة لا تحتوي جميع عناصرها على قواميس
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7
[0,1,2]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT
{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}
الحالة 4: لنرسل قائمة من القواميس تحتوي على قاموس لا يحتوي على المفاتيح الصحيحة
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19
[{"att1":"value1"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT
{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}
الحالة 5: لنرسل قائمة من القواميس تحتوي على قاموس به مفاتيح مفقودة:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18
[{"marié":"oui"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT
{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}
الحالة 6: إرسال قائمة قواميس تحتوي على قاموس به المفاتيح الصحيحة، لكن بعض قيمها خاطئة:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46
[{"marié":"x", "enfants":"x", "salaire":"x"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT
{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}
28.3. عميل الويب

يتم الحصول على المجلد [http-clients/05] (الإصدار 10) في البداية عن طريق نسخ المجلد [http-clients/02] (الإصدار 7). ثم يتم تعديله.
28.3.1. الطبقة [dao]
يتم تنفيذ الطبقة [dao] بواسطة الفئة [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):
…
# طريقة غير مستخدمة
def get_admindata(self) -> AdminData:
pass
# حساب الضريبة
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
…
# حساب الضريبة في الوضع المجمّع
def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
# السماح بترحيل الاستثناءات
# تحويل دافعي الضرائب إلى قائمة قواميس
# الاحتفاظ بالخصائص فقط [marié, enfants, salaire]
list_dict_taxpayers = list(
map(lambda taxpayer:
taxpayer.asdict(included_keys=[
'_TaxPayer__marié',
'_TaxPayer__enfants',
'_TaxPayer__salaire']),
taxpayers))
# الاتصال بالخادم
config_server = self.__config_server
if config_server['authBasic']:
response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
auth=(config_server["user"]["login"],
config_server["user"]["password"]))
else:
response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
# وضع التصحيح؟
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(93, résultat['réponse']['erreurs'])
# من المعروف أن النتيجة قد تم ربطها بالمفتاح [results] الوارد في الرد
list_dict_taxpayers2 = résultat['réponse']['results']
# يتم تحديث القائمة الأولية لدافعي الضرائب بالنتائج المستلمة
for i in range(len(taxpayers)):
# تحديث قائمة دافعي الضرائب [i]
taxpayers[i].fromdict(list_dict_taxpayers2[i])
# هنا تم تحديث المعلمة [taxpayers] بالنتائج الواردة من الخادم
- الأسطر 1-26: يبقى الكود كما كان في الإصدار 7 وفي الإصدارات الأخرى؛
- الأسطر 27-70: تم إدخال طريقة جديدة [calculate_tax_in_bulk_mode] وتتمثل مهمتها في حساب الضريبة لقائمة من دافعي الضرائب؛
- السطر 28: [taxpayers] هي قائمة المكلفين هذه؛
- الأسطر 31-39: يتم التحويل من قائمة كائنات من النوع [TaxPayer] إلى قائمة قواميس باستخدام الدالة [map]؛
- الأسطر 34-38: تقوم دالة لامدا المستخدمة بتحويل كائن من النوع [TaxPayer] إلى قاموس من النوع [dict] يحتوي على المفاتيح [marié, enfants, salaire] فقط. ويتم استخدام المعلمة المسماة [included_keys] الخاصة بالطريقة [BaseEntity.asdict] لهذا الغرض. تجدر الإشارة إلى أنه لمعرفة الأسماء الدقيقة للخصائص التي يجب إدراجها في المعلمات [excluded_keys, included_keys]، يجب استخدام القاموس المُعرَّف مسبقًا [taxpayer.__dict__]؛
- الأسطر 41-48: الاتصال بالخادم ثم الحصول على استجابته HTTP؛
- السطور 44 و48:
- يتم استخدام الطريقة الثابتة [requests.post] لإرسال طلب POST إلى الخادم؛
- يتم استخدام المعلمة المسماة [json] للإشارة إلى أن نص POST عبارة عن سلسلة jSON. وسيكون لذلك نتيجتان:
- سيتم تحويل الكائن المخصص للمعلمة المسماة [json]، وهي في هذه الحالة قائمة من القواميس، إلى سلسلة jSON؛
- الرأس
سيتم تضمينه في الرؤوس HTTP لـ POST؛
- السطر 59: يتم فك تسلسل الاستجابة jSON الواردة من الخادم في القاموس [résultat]؛
- الأسطر 61-63: تتم معالجة أي خطأ محتمل مرسَل من الخادم؛
- السطر 65: توجد نتائج حساب الضريبة في قائمة من القواميس؛
- الأسطر 67-69: تُستخدم هذه النتائج لتحديث القائمة الأولية للمكلفين [taxpayers] التي تم استلامها في البداية بواسطة الطريقة، السطر 28؛
- السطر 70: هنا تم تحديث القائمة الأولية للمكلفين بنتائج حساب الضريبة؛
28.3.2. النص البرمجي الرئيسي [main]
يتطور البرنامج النصي الرئيسي [main] على النحو التالي: يتم تعديل الدالة [thread_function] التي يتم تنفيذها بواسطة الخيوط التي أنشأها العميل فقط. أما باقي الكود فيبقى دون تغيير.
# تنفيذ الطبقة [dao] في مؤشر ترابط
# «taxpayers» هي قائمة بالمكلفين
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
# سجل بداية الخيط
thread_name = threading.current_thread().name
nb_taxpayers = len(taxpayers)
# سجل
logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
# يتم حساب ضريبة دافعي الضرائب
dao.calculate_tax_in_bulk_mode(taxpayers)
# سجل
logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
- السطران 9-10: في حين كان لدينا سابقًا حلقة تمرّ بالتتابع بكل دافع ضرائب إلى الدالة [dao.calculate_tax]، فإننا هنا نقوم باستدعاء واحد فقط للدالة [dao.calculate_tax_in_bulk_mode] التي نمرر إليها جميع دافعي الضرائب؛
28.3.3. تنفيذ العميل
سنقارن أوقات التنفيذ بين الإصدارين:
- الإصدار 7، حيث يتم إرسال كل دافع ضرائب في طلب منفصل باستخدام الطريقة HTTP؛
- الإصدار 10 (هذا الإصدار) حيث يتم تجميع دافعي الضرائب في استعلام واحد HTTP؛
أولاً، الإصدار 6. لمقارنة الإصدارين، نضبط الخاصية [sleep_time] للخادم على الصفر حتى لا يكون هناك انتظار قسري للخيوط. سجلات العميل هي كما يلي:
2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
…
2020-07-28 14:20:45.913065, Thread-3 : 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-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]
وبالتالي، فإن مدة تنفيذ العميل لحساب ضريبة 11 دافع ضرائب هي [913065-811347= 101718]، أي حوالي 102 مللي ثانية.
لنقم بنفس الشيء مع الإصدار 10 (sleep_time من الخادم عند الصفر). عندئذٍ تكون سجلات العميل كما يلي:
2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
….
2020-07-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables
وبالتالي، فإن مدة تنفيذ العميل لحساب ضريبة 11 دافع ضرائب هي [935958-871428= 64530 ns] (السطر 8 – السطر 1)، أي حوالي 65 مللي ثانية. وبذلك، تحقق هذه النسخة الجديدة رقم 10 تحسناً بنسبة 57٪ تقريباً مقارنةً بالنسخة رقم 7.
28.3.4. اختبارات طبقة [dao] للعميل

اختبار [TestHttpClientDao] للعميل في الإصدار 10 قريب جدًا من اختبار الإصدار 7:
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_in_bulk_mode([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 = config["layers"]["dao"]
# يتم تنفيذ طرق الاختبار
print("tests en cours...")
unittest.main()
- السطر 14: بدلاً من استدعاء الأسلوب [dao.calculate_tax]، يتم استدعاء الأسلوب [dao.calculate_tax_in_bulk_mode] الذي يتم تمرير قائمة (وجود الأقواس) بمعلومات أحد دافعي الضرائب إليه؛
تم اجتياز جميع الاختبارات.