Skip to content

28. تمرین عملی: نسخه ۱۰

28.1. مقدمه

در مثال‌های کلاینت سرور محاسبه مالیات، اگر لازم بود N مالیات‌دهنده را پردازش کنند، رشته‌ها درخواست‌های N را به‌صورت متوالی ارسال می‌کردند. ایده اینجا این است که یک درخواست واحد ارسال شود که تمام N مالیات‌دهنده را در بر بگیرد. برای هر یک از آن‌ها، اطلاعات [marié, enfants, salaire] باید ارسال شود. این می‌تواند به‌عنوان پارامترها ارسال شود:

  • به‌عنوان بخشی از درخواست URL. این امر منجر به یک URL طولانی و نسبتاً بی‌معنی می‌شود؛
  • در بدنه درخواست HTTP. ما می‌دانیم که این بخش از دید کاربرانی که از طریق مرورگر به سایت دسترسی دارند، پنهان است؛

در هر دو مورد، می‌توانیم از درخواستی مانند [GET] یا [POST] استفاده کنیم. ما از درخواستی مانند POST با پارامترهایی که در بدنه درخواست HTTP جای گرفته‌اند، استفاده خواهیم کرد.

معماری کلاینت/سرور تغییر نکرده است:

Image

28.2. سرور وب

Image

پوشه [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) را راه‌اندازی می‌کنیم؛

Image

  • به [1]: ما یک درخواست به [POST] ارسال می‌کنیم؛
  • در [2]: URL سرور؛
  • به [3]: بدنه درخواست HTTP;
  • به [5]: مشخص می‌کنیم که این بدنه باید به صورت یک رشته ارسال شود jSON;
  • در [4]: ما به حالت [raw] سوئیچ می‌کنیم تا بتوانیم یک رشته را کپی و پیست کنیم jSON;
  • در [6]: رشته jSON را که از یکی از فایل‌های [résultats.json] نسخه‌های مختلف گرفته شده است، الصاق کنید. سپس، برای هر مؤدی، فقط ویژگی‌های [marié, salaire, enfants] را حفظ کنید؛

Image

  • در [7]، ما به سربرگ‌های HTTP که کلاینت Postman به سرور ارسال می‌کند، نگاه می‌کنیم؛
  • در [8]، می‌بینیم که یک هدر [Content-Type] ارسال خواهد شد که نشان می‌دهد درخواست حاوی بدنه رمزگذاری‌شده در jSON است. این امر ناشی از انتخاب [5] در مرحله قبل است؛

Image

  • در [9-12]: ما شناسه‌های مورد انتظار سرور را در درخواست قرار می‌دهیم؛

ما این درخواست را ارسال می‌کنیم. پاسخ سرور به شرح زیر است:

Image

  • در [3]، ما jSON را دریافت کردیم؛
  • در [4]، مالیات مودیان؛

بیایید دیالوگ کلاینت/سرور را که در کنسول Postman (Ctrl-Alt-C) رخ داده است، بررسی کنیم:

کلاینت پستمن متن زیر را ارسال کرد:

POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000
  },
  {
    "marié": "non",
    "enfants": 2,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 5,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 30000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 200000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 200000
  }
]
  • خط ۱: 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. کلاینت وب

Image

پوشه [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 تبدیل خواهد شد؛
      • سربرگ
Content-Type: application/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] کلاینت

Image

آزمون سمت کلاینت [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] فراخوانی می‌شود که به آن یک لیست (نشان داده شده با کروشه‌ها) از یک مؤدی ارسال می‌شود؛

تمام تست‌ها با موفقیت عبور می‌کنند.