28. تمرین عملی: نسخه ۱۰
28.1. مقدمه
در مثالهای کلاینت سرور محاسبه مالیات، اگر لازم بود N مالیاتدهنده را پردازش کنند، رشتهها درخواستهای N را بهصورت متوالی ارسال میکردند. ایده اینجا این است که یک درخواست واحد ارسال شود که تمام N مالیاتدهنده را در بر بگیرد. برای هر یک از آنها، اطلاعات [marié, enfants, salaire] باید ارسال شود. این میتواند بهعنوان پارامترها ارسال شود:
- بهعنوان بخشی از درخواست URL. این امر منجر به یک URL طولانی و نسبتاً بیمعنی میشود؛
- در بدنه درخواست 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():
…
- خط ۲: 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:
#کلیدهای QZXW2HTMLBW21hcmnDqSwgZW5mYW50cy,gc2FsYWlyZV0ZQX باید در دیکشنری موجود باشند
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'],
'salary': 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
- خط ۹: کنترلکننده دریافت میکند:
- درخواست [request] از سمت کلاینت؛
- پیکربندی سرور [config];
- خطوط ۱۴–۱۸: بدنه POST بازیابی میشود. پارامترهای نهفته در بدنه درخواست HTTP میتوانند به روشهای مختلفی رمزگذاری شوند. ما قبلاً با یکی از آنها مواجه شدهایم: [x-www-form-urlencoded]. در اینجا، از یک رمزگذاری دیگر استفاده خواهیم کرد: jSON;
- خط ۱۸: [request.data] بدنه درخواست HTTP را بازیابی میکند. در اینجا، ما مقداری متن را بازیابی میکنیم و میدانیم که این متن از jSON میآید، که نمایانگر فهرستی از فرهنگلغتها [marié, enfants, salaire] است؛
- خطوط ۱۹–۲۴: این فهرست واژهنامهها بازیابی میشود؛
- خطوط ۲۲–۲۴: اگر بازیابی jSON ناموفق باشد، خطا ثبت میشود؛
- خطوط ۲۶–۲۸: اگر مشخص شود که شیء بازیابیشده یک لیست نیست یا یک لیست خالی است، خطا ثبت میشود؛
- خطوط ۲۹–۳۸: اگر یک لیست با موفقیت بازیابی شده باشد، بررسی میکنیم که آیا واقعاً یک لیست از دیکشنریها است؛
- خطوط ۴۰–۴۳: اگر خطایی رخ داده باشد، در آنجا متوقف شده و یک پاسخ خطا به کلاینت ارسال میکنیم؛
- خطوط ۴۵–۶۹: اکنون هر یک از دیکشنریها را بررسی میکنیم:
- باید شامل کلیدهای [marié, enfants, salaire] باشند؛
- باید امکان ساخت یک شی معتبر [TaxPayer] را فراهم کنند؛
- خطوط ۶۵–۶۹: اگر خطایی در یک دیکشنری تشخیص داده شود، آن را تحت کلید 'error' به همان دیکشنری اضافه میکنیم؛
- خطوط ۷۲–۷۵: فرهنگهای لغت حاوی خطاها به لیست [list_erreurs] اضافه شدهاند. اگر این لیست خالی نباشد، در یک پاسخ خطا برای کلاینت ارسال میشود؛
- خط ۷۷: در این مرحله، میدانیم که میتوانیم از بدنه درخواست ارسالشده توسط کلاینت، یک لیست از اشیاء از نوع [TaxPayer] ایجاد کنیم؛
- خطوط ۸۴–۹۱: ما فهرست فرهنگهای دریافتی را پردازش میکنیم؛
- خط ۸۶: از یک دیکشنری، یک شیء [TaxPayer] ایجاد میکنیم؛
- خط ۸۹: مالیات برای این [TaxPayer] محاسبه میشود؛
- خط ۹۱: میدانیم که [taxpayer] توسط محاسبه مالیات تغییر کرده است. آن را به یک دیکشنری تبدیل کرده و به فهرست نتایج اضافه میکنیم؛
- خط ۹۳: این فهرست نتایج به کلاینت ارسال میشود؛
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) رخ داده است، بررسی کنیم:
کلاینت پستمن متن زیر را ارسال کرد:
- خط ۱: POST به سرور ارسال شد؛
- خط ۲: هدر احراز هویت HTTP؛
- خط ۳: کلاینت به سرور میگوید که یک رشته با شناسه jSON را ارسال میکند و این رشته ۸۲۴ بایت طول دارد (خط ۱۱);
- خطوط ۱۳–۶۹: بدنه 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}]}}
- خط ۱: درخواست با موفقیت انجام شد؛
- خط ۲: بدنه پاسخ سرور یک رشته jSON است. این رشته ۱۴۶۱ بایت طول دارد (خط ۳)؛
- خط ۷: پاسخ سرور 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: 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)"]}}
- خط ۱۳: ما رشته [abc] را ارسال کردیم که یک رشته معتبر jSON نیست (خط ۳);
- خط ۱۵: سرور با کد خطای ۴۰۰ پاسخ میدهد؛
- خط ۲۱: پاسخ سرور jSON;
مورد ۲: بیایید یک رشته معتبر 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"]}}
مورد ۳: بیایید یک رشته 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"]}}
مورد ۴: بیایید یک لیست از دیکشنریها ارسال کنیم که شامل دیکشنریای باشد که کلیدهای صحیح را ندارد
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]"}]}}
مورد ۵: بیایید یک لیست از دیکشنریها ارسال کنیم که شامل دیکشنریای با کلیدهای ناقص است:
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]"}]}}
مورد ۶: بیایید یک لیست از دیکشنریها را ارسال کنیم که شامل یک دیکشنری با کلیدهای صحیح و برخی با مقادیر نادرست باشد:
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] (نسخه ۱۰) در ابتدا با کپی کردن پوشه [http-clients/02] (نسخه ۷) ایجاد میشود. سپس اصلاح میشود.
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:
# اجازه میدهد استثناها منتقل شوند
# تبدیل مودیان به فهرستی از فرهنگها
#فقط ویژگیهای QZXW2HTMLBW21hcmnDqSwgZW5mYW50cy، c2FsYWlyZV0ZQX را نگه میدارد
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] با نتایج دریافتشده از سرور بهروزرسانی شده است
- خطوط ۱–۲۶: کد همانطور که در نسخه ۷ و نسخههای دیگر بود، باقی میماند؛
- خطوط ۲۷–۷۰: یک متد جدید، [calculate_tax_in_bulk_mode]، معرفی شده است که هدف آن محاسبه مالیات برای فهرستی از مودیان مالیاتی است؛
- خط ۲۸: [taxpayers] این لیست از مالیاتدهندگان است؛
- خطوط ۳۱–۳۹: یک لیست از اشیاء از نوع [TaxPayer] با استفاده از تابع [map] به یک لیست از دیکشنریها تبدیل میشود؛
- خطوط ۳۴–۳۸: تابع لامبدا مورد استفاده یک شیء از نوع [TaxPayer] را به یک دیکشنری از نوع [dict] تبدیل میکند که تنها شامل کلیدهای [marié, enfants, salaire] است. برای این کار، از پارامتر با نام [included_keys] در متد [BaseEntity.asdict] استفاده میشود. توجه داشته باشید که برای یافتن نامهای دقیق ویژگیهایی که باید در پارامترهای [excluded_keys, included_keys] گنجانده شوند، باید از فرهنگ لغت از پیش تعریفشده [taxpayer.__dict__] استفاده کنید؛
- خطوط ۴۱–۴۸: به سرور متصل شوید و پاسخ آن را بازیابی کنید HTTP;
- خطوط ۴۴، ۴۸:
- متد استاتیک [requests.post] برای ارسال یک POST به سرور استفاده میشود؛
- پارامتر با نام [json] برای نشان دادن اینکه بدنه POST یک رشته jSON است، استفاده میشود. این دو نتیجه خواهد داشت:
- شیء تخصیصیافته به پارامتر با نام [json] – در این مورد، یک لیست از دیکشنریها – به رشته jSON تبدیل خواهد شد؛
- سربرگ
در سربرگهای HTTP از POST گنجانده خواهد شد؛
- خط ۵۹: پاسخ سرور jSON به واژهنامه [résultat] دِسیریلایز میشود؛
- خطوط ۶۱–۶۳: هرگونه خطایی که توسط سرور ارسال میشود، رسیدگی میشود؛
- خط ۶۵: نتایج محاسبه مالیات در یک لیست از فرهنگها ذخیره میشوند؛
- خطوط ۶۷–۶۹: این نتایج برای بهروزرسانی فهرست اولیه مالیاتدهندگان [taxpayers] که در ابتدا توسط متد در خط ۲۸ دریافت شده بود، استفاده میشوند؛
- خط ۷۰: در اینجا، فهرست اولیه مودیان با نتایج محاسبه مالیات بهروزرسانی شده است؛
28.3.2. اسکریپت اصلی [main]
اسکریپت اصلی [main] به شرح زیر تغییر میکند: تنها تابع [thread_function] که توسط نخهای ایجادشده توسط مشتری اجرا میشود، اصلاح شده است. سایر کدها بدون تغییر باقی میمانند.
#اجرای لایه [dao] در یک نخ
# 'مالیاتدهندگان' یک فهرست از مالیاتدهندگان است
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")
- خطوط ۹–۱۰: در حالی که قبلاً یک حلقه وجود داشت که هر مودی را به ترتیب به متد [dao.calculate_tax] پاس میداد، در اینجا یک فراخوانی واحد به متد [dao.calculate_tax_in_bulk_mode] انجام میشود که تمام مودیان به آن پاس داده میشوند؛
28.3.3. اجرای کلاینت
زمانهای اجرای نسخهها را مقایسه خواهیم کرد:
- ۷، که در آن هر مودی موضوع یک پرسوجوی HTTP است؛
- ۱۰ (این نسخه)، که در آن مالیاتدهندگان در یک پرسوجوی واحد HTTP گروهبندی شدهاند؛
ابتدا، نسخهٔ ۶. برای مقایسهٔ این دو نسخه، ویژگی [sleep_time] سرور را روی صفر تنظیم میکنیم تا هیچ نخ (thread) اجباری در انتظار نباشد. لاگهای کلاینت به شرح زیر است:
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]
بنابراین زمان اجرای کلاینت برای محاسبه مالیات برای ۱۱ مالیاتدهنده [913065-811347= 101718] است، یعنی تقریباً ۱۰۲ میلیثانیه.
بیایید همین کار را با نسخه ۱۰ (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
زمان اجرای کلاینت برای محاسبه مالیات برای ۱۱ مالیاتدهنده بنابراین [935958-871428= 64530 ns] (خط ۸ – خط ۱) است، یعنی تقریباً ۶۵ میلیثانیه. این نسخه جدید ۱۰ در نتیجه، افزایش عملکردی در حدود ۵۷ درصد را نسبت به نسخه ۷ ارائه میدهد.
28.3.4. آزمایشهای لایه [dao] کلاینت

آزمون سمت کلاینت [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_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()
- خط ۱۴: به جای فراخوانی متد [dao.calculate_tax]، متد [dao.calculate_tax_in_bulk_mode] فراخوانی میشود که به آن یک لیست (نشان داده شده با کروشهها) از یک مؤدی ارسال میشود؛
تمام تستها با موفقیت عبور میکنند.