Skip to content

24. تمرین عملی: نسخه ۷

24.1. مقدمه

نسخهٔ ۷ اپلیکیشن محاسبه مالیات، به جز جزئیات زیر، با نسخهٔ ۶ یکسان است:

  • کلاینت وب چندین درخواست HTTP را به‌طور همزمان اجرا خواهد کرد. در نسخه قبلی، این درخواست‌ها به‌صورت متوالی اجرا می‌شدند. بنابراین سرور در هر لحظه تنها یک درخواست را پردازش می‌کرد؛
  • سرور چندرشته‌ای خواهد بود: قادر خواهد بود چندین درخواست را به‌طور همزمان پردازش کند؛
  • برای نظارت بر اجرای این درخواست‌ها، وب‌سرور به یک ابزار ثبت وقایع (لاگر) مجهز خواهد شد که لحظات کلیدی در پردازش درخواست‌ها را در یک فایل متنی ثبت می‌کند؛
  • سرور در صورتی که با مشکلی مواجه شود که مانع از راه‌اندازی آن شود، معمولاً مشکلی در پایگاه داده مرتبط با وب سرور، یک ایمیل برای مدیر برنامه ارسال خواهد کرد؛

معماری برنامه بدون تغییر باقی می‌ماند:

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

Image

پوشه [http-servers/02] ابتدا با کپی کردن پوشه [http-servers/01] ایجاد می‌شود. سپس اصلاحاتی روی آن انجام می‌شود.

24.2. ابزارها

Image

24.2.1. کلاس [Logger]

کلاس [Logger] امکان ثبت برخی از عملیات سرور وب در یک فایل متنی را فراهم می‌کند:


import codecs
import threading
from datetime import date, datetime
from threading import current_thread

from ImpôtsError import ImpôtsError


class Logger:
    # ویژگی کلاس
    verrou = threading.RLock()

    # سازنده
    def __init__(self, logs_filename: str):
        try:
            # باز کردن فایل در حالت الحاق (a)
            self.__resource = codecs.open(logs_filename, "a", "utf-8")
        except BaseException as erreur:
            raise ImpôtsError(18, f"{erreur}")

    # نوشتن لاگ
    def write(self, message: str):
        # تاریخ و زمان فعلی
        today = date.today()
        now = datetime.time(datetime.now())
        #نام نخ
        thread_name = current_thread().name
        # ما نمی‌خواهیم هنگام نوشتن در فایل لاگ متوقف شویم
        #درخواست شیء همگام‌سازی (= قفل) برای کلاس – تنها یک نخ آن را به‌دست می‌آورد
        Logger.verrou.acquire()
        try:
            # نوشتن در لاگ
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            # فوراً بنویسید – در غیر این صورت متن تنها زمانی نوشته می‌شود که جریان نوشتن بسته شود
            # یا می‌خواهیم گزارش‌ها را در طول زمان ردیابی کنیم
            self.__resource.flush()
        finally:
            # ما شیء همگام‌سازی (= قفل) را آزاد می‌کنیم تا نخ دیگری بتواند آن را به دست آورد
            Logger.verrou.release()

    #آزادسازی منابع
    def close(self):
        # بستن فایل
        if self.__resource:
            self.__resource.close()
  • خطوط ۱۰–۱۱: یک ویژگی کلاس تعریف می‌شود. ویژگی کلاس، صفتی است که بین تمام نمونه‌های یک کلاس مشترک است. به آن با استفاده از نشانه [Classe.attribut_de_classe] (خطوط ۳۰، ۳۹) ارجاع داده می‌شود. ویژگی کلاس [verrou] به عنوان یک شی همگام‌سازی برای تمام رشته‌هایی که کد موجود در خطوط ۳۱–۳۶ را اجرا می‌کنند، عمل خواهد کرد؛
  • خطوط 14–19: سازنده (constructor) مسیر مطلق فایل لاگ را دریافت می‌کند. سپس این فایل باز شده و توصیف‌گر فایل (file descriptor) به دست آمده در کلاس ذخیره می‌شود؛
  • خط ۱۷: فایل لاگ در حالت «append» (a) باز می‌شود. هر خط نوشته شده به انتهای فایل اضافه خواهد شد؛
  • خطوط ۲۲–۳۹: متد [write] امکان نوشتن پیامی را که به‌عنوان پارامتر ارسال شده است، در فایل لاگ فراهم می‌کند. دو مورد اطلاعات به این پیام اضافه می‌شوند:
    • خط ۲۴: تاریخ جاری؛
    • خط 25: زمان فعلی؛
    • خط ۲۷: نام ترید نویسنده لاگ. در اینجا مهم است که به خاطر داشته باشیم یک اپلیکیشن وب همزمان به چندین کاربر خدمت‌رسانی می‌کند. به هر درخواست یک ترید برای اجرا اختصاص داده می‌شود. اگر این ترید متوقف شود، معمولاً برای یک عملیات ورودی/خروجی (شبکه، فایل‌ها، پایگاه داده)، پردازنده به ترید دیگری اختصاص داده خواهد شد. به دلیل این وقفه‌های احتمالی، نمی‌توانیم مطمئن باشیم که یک تِرد بتواند بدون وقفه، یک خط را در فایل لاگ بنویسد. بنابراین این خطر وجود دارد که لاگ‌های دو تِرد مختلف با هم مخلوط شوند. این خطر کم است، شاید حتی وجود نداشته باشد، اما با این وجود ما تصمیم گرفته‌ایم نشان دهیم چگونه دسترسی دو تِرد به یک منبع مشترک، در این مورد فایل لاگ، را همگام‌سازی کنیم؛
  • خط ۳۰: قبل از نوشتن، تِرد کلیدِ درِ ورودی را درخواست می‌کند. کلیدِ درخواست‌شده همان کلیدی است که در خط ۱۱ ایجاد شده است. این کلید واقعاً منحصربه‌فرد است: یک ویژگی کلاس در تمام نمونه‌های آن کلاس منحصربه‌فرد است؛
    • در زمان T1، یک رشته با نام Thread1 کلید را به دست می‌آورد. سپس می‌تواند خط ۳۳ را اجرا کند؛
    • در زمان T2، نخ Thread1 قبل از اینکه حتی نوشتن لاگ را تمام کند، متوقف می‌شود؛
    • در زمان T3، نخ Thread2 که پردازنده را به دست آورده است، باید یک لاگ نیز بنویسد. بدین ترتیب به خط ۳۰ می‌رسد، جایی که کلید درِ ورودی را درخواست می‌کند. به او گفته می‌شود که نخ دیگری قبلاً آن را در اختیار دارد. سپس به طور خودکار متوقف می‌شود. همین اتفاق برای تمام نخ‌هایی که این کلید را درخواست می‌کنند نیز خواهد افتاد؛
    • در زمان T4، نخ Thread1 که متوقف شده بود، پردازنده را دوباره به دست می‌آورد. سپس نوشتن لاگ را به پایان می‌رساند؛
  • خطوط ۳۲–۳۶: نوشتن در فایل لاگ در دو مرحله انجام می‌شود:
  • خط ۳۳: توصیف‌کنندهٔ فایل که در خط ۱۷ به‌دست آمده، با یک بافر کار می‌کند. عملیات [write] در خط ۳۳ به این بافر می‌نویسد اما مستقیماً به فایل نمی‌نویسد. سپس بافر تحت شرایط خاصی به فایل تخلیه می‌شود:
        • فایل دیسکریپتور تحت عملیاتی از نوع [close] یا [flush] قرار گیرد؛
        • فایل دیسکریپتور تحت عملیات [close] یا [flush] قرار می‌گیرد؛
  • خط ۳۶: نوشتن خط لاگ به فایل اجباری می‌شود. ما این کار را انجام می‌دهیم زیرا می‌خواهیم لاگ‌های مربوط به نخ‌های مختلف در میان یکدیگر درآمیزند. اگر این کار را انجام ندهیم، تمام لاگ‌های یک نخ به طور همزمان هنگام بستن توصیف‌گر فایل در خط ۴۵ نوشته خواهند شد. در این صورت، تشخیص اینکه برخی از نخ‌ها متوقف شده‌اند، بسیار دشوارتر خواهد بود: ما باید زمان‌بندی‌های موجود در لاگ‌ها را بررسی کنیم؛
  • خط ۳۹: نخ Thread1 قفلی را که به آن اعطا شده بود، بازمی‌گرداند. سپس می‌توان آن را به نخ دیگری اعطا کرد؛
  • خط ۲۲: بنابراین متد [write] همگام‌سازی شده است: تنها یک نخ در هر زمان به فایل لاگ چیزی می‌نویسد. کلید این مکانیزم خط ۳۰ است: هر چه پیش بیاید، تنها یک نخ کلید را برای ادامه به خط بعدی بازیابی می‌کند. این نخ کلید را تا زمانی که آن را بازگرداند (خط ۳۹) نگه می‌دارد؛
  • خطوط ۴۱–۴۵: متد [close] منابع تخصیص‌یافته به توصیف‌گر فایل لاگ را آزاد می‌کند؛

گزارش‌های نوشته شده در فایل گزارش به شکل زیر خواهند بود:

2020-07-22 20:03:52.992152, Thread-2 : …

24.2.2. کلاس [SendAdminMail]

کلاس [SendAminMail] برای ارسال پیام به مدیر برنامه هنگامی که برنامه از کار می‌افتد (crash) استفاده می‌شود.

Image

کلاس [SendAdminMail] در اسکریپت [config] [2] به صورت زیر پیکربندی شده است:


        #پیکربندی سرور SMTP
        "adminMail": {
            # سرور SMTP
            "smtp-server": "localhost",
            # پورت سرور SMTP
            "smtp-port": "25",
            # مدیر
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            # موضوع ایمیل
            "subject": "plantage du serveur de calcul d'impôts",
            # اگر سرور SMTP به احراز هویت نیاز دارد، TLS را روی True تنظیم کنید، در غیر این صورت روی False تنظیم کنید
            "tls": False
        }

کلاس [SendAdminMail] واژه‌نامه را از خطوط ۲ تا ۱۳ و همچنین پیکربندی ارسال ایمیل را دریافت می‌کند. کلاس به شرح زیر است:


# وارد می‌کند
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


class SendAdminMail:

    # -----------------------------------------------------------------------
    @staticmethod
    def send(config: dict, message: str, verbose: bool = False):
        # یک پیام به سرور SMTP config['smtp-server'] در پورت config[smtp-port] ارسال می‌کند
        #اگر config['tls'] true باشد، از قالب TLS استفاده خواهد شد
        #ایمیل به نمایندگی از config['from'] ارسال می‌شود
        # برای گیرنده config['to']
        # پیام دارای موضوع config['subject'] است
        # ارجاع به یک لاگر را می‌توان در پیکربندی یافت ['logger']

        # لاگ‌گیر را از پیکربندی بازیابی می‌کند – ممکن است روی None تنظیم شود
        logger = config["logger"]
        # سرور SMTP
        server = None
        #پیام ارسال می‌شود
        try:
            # سرور SMTP
            server = smtplib.SMTP(config["smtp-server"])
            # حالت تفصیلی
            server.set_debuglevel(verbose)
            # اتصال امن؟
            if config['tls']:
                # شروع دیالوگ امنیتی
                server.starttls()
                # احراز هویت
                server.login(config["user"], config["password"])
            # در حال ساخت یک پیام چندبخشی – این پیامی است که ارسال خواهد شد
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            #ارسال پیام
            server.send_message(msg)
            # لاگ – ممکن است لاگ‌گیر وجود نداشته باشد
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            # لاگ – ممکن است لاگ‌گیر وجود نداشته باشد
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            #کار تمام شد – منابع مصرفی تابع آزاد می‌شوند
            if server:
                server.quit()
  • خطوط 24–54: این شامل کدی است که قبلاً در مثال |smtp/02| بررسی شده است؛
  • خط ۲۰: یک مرجع لاگر بازیابی می‌شود. این در خطوط ۴۵ و ۴۹ استفاده می‌شود؛

24.3. سرور وب

Image

24.3.1. پیکربندی

Image

پیکربندی سرور بسیار شبیه به پیکربندی سروری است که قبلاً بررسی شد. تنها فایل [config.py] کمی تغییر کرده است:


def configure(config: dict) -> dict:
    import os

    # مرحله ۱ ------

    # پوشهٔ این فایل
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # مسیر ریشه
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #وابستگی‌های مطلق
    absolute_dependencies = [
        # پوشه‌های پروژه
        #BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #ثابت‌ها، بازه‌ها
        f"{root_dir}/impots/v05/entities",
        # IndexController
        f"{root_dir}/impots/http-servers/01/controllers",
        #اسکریپت‌های [config_database, config_layers]
        script_dir,
        # لاگر، SendAdminMail
        f"{script_dir}/../utilities",
    ]
    # مسیر سیستم را تنظیم کنید
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    # پیکربندی برنامه
    config.update({
        # کاربران مجاز به استفاده از برنامه
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],
        # فایل لاگ
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # پیکربندی سرور SMTP
        "adminMail": {
            # سرور SMTP
            "smtp-server""localhost",
            # پورت سرور SMTP
            "smtp-port""25",
            # مدیر
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # موضوع ایمیل
            "subject""plantage du serveur de calcul d'impôts",
            # اگر سرور SMTP به احراز هویت نیاز دارد، TLS را روی True تنظیم کنید، در غیر این صورت روی False تنظیم کنید
            "tls"False
        },
        #مدت زمان مکث نخ در ثانیه
        "sleep_time"0
    })

    # مرحله ۳ ------
    #پیکربندی پایگاه داده
    import config_database
    config["database"] = config_database.configure(config)

    # مرحله ۴ ------
    # نمادسازی لایه‌های کاربردی
    import config_layers
    config['layers'] = config_layers.configure(config)

    #پیکربندی بازگردانده می‌شود
    return config
  • خطوط ۴۰–۶۶: ما ورودی‌هایی را به فرهنگ لغت پیکربندی سرور اضافه می‌کنیم که مربوط به لاگ‌گیر (خط ۴۹) و مواردی است که به ارسال ایمیل هشدار به مدیر برنامه مربوط می‌شود (خطوط ۵۱–۶۳)؛
  • خط ۶۵: برای مشاهده بهتر اجرای نخ‌ها، برخی از آن‌ها را مجبور به توقف می‌کنیم. [sleep_time] مدت زمان توقف را، که به ثانیه بیان شده است، مشخص می‌کند؛
  • خطوط 27–28: توجه کنید که ما از کنترل‌کننده [index_controller] نسخه 6 قبلی استفاده می‌کنیم؛

24.3.2. اسکریپت اصلی [main]

اسکریپت اصلی [main] به شرح زیر است:


# انتظار یک پارامتر MySQL یا PostgreSQL
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#پیکربندی برنامه
import config
config = config.configure({'sgbd': sgbd})

#وابستگی‌ها
from flask import request, Flask
from flask_httpauth import HTTPBasicAuth
import json
import index_controller
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError

# مدیر احراز هویت
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    # فهرست کاربران
    users = config['users']
    # پیمایش این لیست
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # چیزی یافت نشد
    return False


# ارسال ایمیل به مدیر
def send_adminmail(config: dict, message: str):
    # ایمیلی برای مدیر برنامه ارسال می‌شود
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


# در حال بررسی فایل لاگ
logger = None
erreur = False
message_erreur = None
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # لاگ کنسول
    print(f"L'erreur suivante s'est produite : {exception}")
    #ثبت خطا
    erreur = True
    message_erreur = f"{exception}"
#ذخیره کردن لاگ‌گیر در پیکربندی
config['logger'] = logger
# پردازش خطا
if erreur:
    # ایمیل به مدیر
    send_adminmail(config, message_erreur)
    # برنامه خاتمه می‌یابد
    sys.exit(1)

# فایل لاگ راه‌اندازی
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

#بازیابی داده‌ها از مراجع مالیاتی
erreur = False
try:
    #داده‌های مدیریتی به صورت فقط خواندنی و در سطح برنامه خواهند بود
    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)

# رشته اصلی دیگر به لاگ‌گیر نیاز ندارد
logger.close()

# اگر خطایی رخ داده باشد، فرآیند متوقف می‌شود
if erreur:
    sys.exit(2)

# برنامه Flask می‌تواند شروع شود
app = Flask(__name__)


# خانه URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():



#فقط اصلی
if __name__ == '__main__':
    # راه‌اندازی سرور
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • خطوط ۱–۱۰: اسکریپت منتظر یک پارامتر [mysql / pgres] است که به آن می‌گوید کدام SGBD را باید استفاده کند؛
  • خطوط ۱۲–۱۴: پیکربندی برنامه (مسیر پایتون، لایه‌ها، پایگاه داده)؛
  • خطوط ۱۶–۲۸: وابستگی‌های مورد نیاز برنامه؛
  • خطوط ۳۰–۴۳: مدیریت احراز هویت؛
  • خطوط ۴۶–۵۱: تابعی که ایمیلی به مدیر برنامه ارسال می‌کند؛
  • این تابع دو پارامتر را انتظار دارد:
      • config: یک دیکشنری با کلیدهای [adminMail] و [logger]؛
      • پیامی که باید ارسال شود؛
    • خطوط ۴۹–۵۰: پیکربندی ایمیل انجام می‌شود؛
    • ایمیل ارسال می‌شود؛
  • خطوط ۵۴–۷۴: وجود فایل لاگ را بررسی می‌کنیم؛
  • خطوط ۷۰–۷۴: اگر فایل لاگ باز نشود، ایمیلی برای مدیر ارسال شده و فرآیند متوقف می‌شود؛
  • خطوط ۷۶–۷۹: ثبت راه‌اندازی سرور؛
  • خطوط ۸۱–۹۸: ما داده‌های مرجع مالیاتی را از پایگاه داده بازیابی می‌کنیم؛
  • خطوط ۸۸–۹۸: اگر نتوانیم این داده‌ها را بازیابی کنیم، خطا را هم در کنسول و هم در فایل لاگ ثبت می‌کنیم؛
  • خطوط 100–101: نخ اصلی دیگر به لاگ نویسی نخواهد کرد (نخ‌های ایجادشده از همان توصیف‌گر فایل استفاده نخواهند کرد)؛
  • خطوط ۱۰۳–۱۰۵: اگر نتوانستیم به پایگاه داده متصل شویم، متوقف می‌شویم؛
  • خط ۱۲۲: سرور در حالت چندرشته‌ای راه‌اندازی می‌شود؛

تابع [index] (خط 114) به شرح زیر است:


# خانه URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        # لاگ‌گیر
        logger = Logger(config["logsFilename"])
        #آن را در پیکربندی مرتبط با نخ ذخیره کنید
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #ثبت درخواست
        logger.write(f"[index] requête : {request}\n")
        # اگر درخواست شود، نخ متوقف می‌شود
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #وقفه به‌صورت تصادفی تنظیم می‌شود، به‌طوری که برخی از رشته‌ها قطع می‌شوند و برخی دیگر قطع نمی‌شوند
            aléa = randint(0, 1)
            if aléa == 1:
                # لاگ قبل از مکث
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #مکث
                time.sleep(sleep_time)
        # درخواست توسط یک کنترل‌کننده اجرا می‌شود
        résultat, status_code = index_controller.execute(request, config)
        #آیا خطای فاتال رخ داده است؟
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # ایمیلی برای مدیر برنامه ارسال می‌شود
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        # پاسخ ثبت می‌شود
        logger.write(f"[index] {résultat}\n")
        # ارسال پاسخ
        return json_response(résultat, status_code)
    except BaseException as erreur:
        # در صورت امکان خطا را ثبت کنید
        if logger:
            logger.write(f"[index] {erreur}")
        # آماده‌سازی پاسخ برای مشتری
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #ارسال پاسخ
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # اگر فایل لاگ باز است، آن را ببندید
        if logger:
            logger.close()
  • خط ۴: تابع ای که هنگام درخواست کاربر برای URL اجرا می‌شود. از آنجایی که سرور چندرشته‌ای است (خط ۱۱۲)، یک رشته برای اجرای این تابع ایجاد خواهد شد. این رشته را می‌توان در هر زمان متوقف و مکث کرد تا کمی بعد اجرای آن از سر گرفته شود. هرگاه کد به منبعی دسترسی پیدا می‌کند که توسط همه رشته‌ها مشترک است، مهم است که این موضوع را در نظر داشته باشیم. در این مورد، چنین منبعی فایل لاگ است: همه رشته‌ها در آن می‌نویسند؛
  • خط ۸: یک نمونه از لاگ‌گیر ایجاد می‌شود. بنابراین، همه رشته‌ها نمونه متفاوتی از لاگ‌گیر خواهند داشت. با این حال، همه این لاگ‌گیرها به یک فایل لاگ اشاره می‌کنند. با این وجود، مهم است که توجه داشته باشیم وقتی یک رشته لاگ‌گیر خود را می‌بندد، این کار تأثیری بر لاگ‌گیرهای سایر رشته‌ها ندارد؛
  • خطوط ۹–۱۲: لاگر در دیکشنری برنامه [config] ذخیره می‌شود، که با کلیدی حاوی نام تِرد مرتبط است. بنابراین، اگر n نخ به طور همزمان در حال اجرا باشند، n مدخل در دیکشنری [config] ایجاد خواهد شد. [config] منبعی است که در میان تمام نخ‌ها مشترک است. بنابراین ممکن است نیازی به همگام‌سازی باشد. من در اینجا یک فرض کرده‌ام. من فرض کرده‌ام که اگر دو نخ همزمان ورودی‌های خود را در فایل [config] ایجاد کنند و یکی از آن‌ها توسط دیگری قطع شود، این امر تأثیری نخواهد داشت. نخ قطع‌شده می‌تواند بعداً ایجاد ورودی خود را تکمیل کند. اگر آزمایش نشان دهد که این فرض نادرست است، دسترسی به خط ۱۲ باید همگام‌سازی شود؛
  • خط ۱۰: لاگر در یک دیکشنری قرار می‌گیرد؛
  • خط ۱۱: [threading.current_thread()] رشته‌ای است که این خط را اجرا می‌کند و در نتیجه رشته‌ای است که تابع [index] را اجرا می‌کند. ما نام آن را ثبت می‌کنیم. هر رشته نام منحصربه‌فردی دارد؛
  • خط ۱۲: ما پیکربندی نخ را ذخیره می‌کنیم. از این پس، ما همیشه به شرح زیر عمل خواهیم کرد: اگر اطلاعاتی وجود داشته باشد که نتوان آن را بین نخ‌ها به اشتراک گذاشت، آن را همچنان در پیکربندی عمومی قرار می‌دهیم، اما با نام نخ مرتبط می‌سازیم؛
  • خط ۱۴: ما درخواست در حال اجرا را ثبت می‌کنیم؛
  • خطوط ۱۵–۲۴: ما به طور تصادفی برخی از رشته‌ها را متوقف می‌کنیم تا پردازنده را به رشته دیگری واگذار کنند؛
    • خط ۱۶: مدت زمان توقف (به ثانیه) از پیکربندی بازیابی می‌شود؛
    • خط 17: وقفه تنها در صورتی رخ می‌دهد که مدت زمان وقفه 0 نباشد؛
    • خط ۱۹: یک عدد صحیح تصادفی در بازه [0, 1]. بنابراین، تنها مقادیر ۰ و ۱ ممکن هستند؛
    • خط ۲۰: نخ تنها در صورتی متوقف می‌شود که عدد تصادفی ۱ باشد؛
    • خط ۲۲: یک ورودی لاگ ثبت می‌شود تا نشان دهد که نخ در آستانه توقف قرار دارد؛
    • خط ۲۴: نخ برای [sleep_time] ثانیه معلق می‌شود؛
  • خط ۲۶: هنگامی که نخ از سر گرفته می‌شود، پرس‌وجو توسط ماژول [index_controller] اجرا می‌شود؛
  • خطوط ۲۸–۳۲: اگر این اجرا باعث خطایی از نوع [500 INTERNAL SERVER ERROR] شود، ایمیلی برای مدیر ارسال می‌شود؛
    • خطوط ۳۰–۳۱: دیکشنری [config_mail] پیکربندی شده و به کلاس [SendAdminMail] پاس داده می‌شود؛
    • خط ۳۲: پیامی که برای مدیر ارسال می‌شود، رشته jSON از نتیجه‌ای است که به کلاینت ارسال خواهد شد؛
  • خطوط ۳۳–۳۴: ما پاسخ را که قرار است به کلاینت ارسال کنیم، ثبت می‌کنیم (خط ۳۶);
  • خطوط ۳۷–۴۴: رسیدگی به هرگونه استثنا؛
  • خطوط ۳۹–۴۰: اگر لاگ‌گیر وجود داشته باشد، خطایی را که رخ داده است لاگ می‌کنیم؛
  • خطوط ۴۷–۴۸: بستن لاگر در صورت وجود. در نهایت، تِرد در ابتدای درخواست یک لاگر ایجاد می‌کند و پس از پردازش درخواست، آن را می‌بندد؛

24.3.3. کنترل‌کننده [index_controller]

کنترل‌کننده [index_controller] که درخواست‌ها را اجرا می‌کند، همانند نسخه قبلی است:

Image

24.3.4. اجرا

ما سرور Flask، سرور ایمیل |hMailServer| و کلاینت ایمیل |Thunderbird| را راه‌اندازی می‌کنیم. ما SGBD را راه‌اندازی نمی‌کنیم. سرور با لاگ‌های کنسول زیر متوقف می‌شود:


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-servers/02/flask/main.py mysql
[serveur] démarrage du serveur
L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]

Process finished with exit code 2 

فایل لاگ [logs.txt] به شرح زیر است:


2020-07-23 11:51:38.324752, MainThread : [serveur] démarrage du serveur
2020-07-23 11:51:40.355510, MainThread : L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
2020-07-23 11:51:42.464206, MainThread : [SendAdminMail] Message envoyé à [guest@localhost.com] : [L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]]

با استفاده از Thunderbird، ایمیل‌های مدیر را بررسی می‌کنیم ([guest@localhost.com]):

Image

سپس SGBD را اجرا کرده و URL و [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000] را درخواست می‌کنیم. لاگ‌ها به شکل زیر درمی‌آیند:


2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=3&salary=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [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}}}
  • خطوط ۱–۴: توجه کنید که سرور دو بار راه‌اندازی می‌شود زیرا حالت [Debug=True] یک راه‌اندازی دوم را فعال می‌کند؛
  • خطوط ۵–۶: لاگ‌ها به ما ایده‌ای از زمان اجرای یک درخواست می‌دهند، در این مورد ۲.۲۹۳ میلی‌ثانیه؛

24.4. کلاینت وب

Image

Image

پوشه [http-clients/02] با کپی کردن پوشه [http-clients/01] ایجاد می‌شود. سپس چند تغییر اعمال می‌شود.

24.4.1. پیکربندی

پیکربندی [config] برای برنامه [http-clients/02 به جز چند جزئیات جزئی، با پیکربندی برنامه [http-clients/01] یکسان است:


def configure(config: dict) -> dict:
    import os

    # مرحله ۱ ------

    #پوشه‌ای که حاوی این فایل است
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # مسیر ریشه
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #وابستگی‌های مطلق
    absolute_dependencies = [
        # پوشه‌های پروژه
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #ثابت‌ها، محدوده‌ها
        f"{root_dir}/impots/v05/entities",
        #ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # اسکریپت‌های پیکربندی
        script_dir,
        # لاگ‌گیر
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    # مسیر سیستم را تنظیم کنید
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    #پیکربندی برنامه با ثابت‌ها
    config.update({
        # فایل مالیات‌دهندگان
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # فایل نتایج
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # فایل خطاها
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # فایل لاگ
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # سرور محاسبه مالیات
        "server": {
            "urlServer""http://127.0.0.1:5000/",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            }
        },
        # حالت اشکال‌زدایی
        "debug"True
    }
    )

    # مرحله ۳ ------
    # مصالحه‌سازی لایه‌ها
    import config_layers
    config['layers'] = config_layers.configure(config)

    # اعمال پیکربندی
    return config
  • خطوط ۳۱–۳۲: ما از همان لاگر |Logger| که برای سرور استفاده می‌شود، استفاده خواهیم کرد؛
  • خط ۴۹: مسیر مطلق فایل لاگ؛
  • خط ۶۰: حالت [debug=True] برای نوشتن پاسخ‌های وب‌سرور در فایل لاگ استفاده می‌شود؛

24.4.2. لایه [dao]

کد کلاس [ImpôtsDaoWithHttpClient] کمی تغییر کرده است:


# واردات‌ها

import requests
from flask_api import status




class ImpôtsDaoWithHttpClient(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

    # روش بلااستفاده
    def get_admindata(self) -> AdminData:
        pass

    # محاسبه مالیات
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # اجازه دهید استثناءها منتقل شوند
        
        # حالت اشکال‌زدایی؟
        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
        
  • خط ۱۷: پیکربندی عمومی ذخیره می‌شود. بعداً خواهیم دید که وقتی سازنده کلاس [ImpôtsDaoWithHttpClient] اجرا می‌شود، دیکشنری [config] هنوز کلید [logger] که در خط ۳۷ استفاده شده را ندارد. به همین دلیل [self.__logger] (خط ۲۳) نمی‌تواند در سازنده مقداردهی اولیه شود؛
  • خط ۲۱: یک کلید [debug] به پیکربندی اضافه شده است که ثبت‌رویداد (logging) برای خطوط ۳۳–۳۹ را کنترل می‌کند؛
  • خط ۳۴: اگر در حالت [debug] باشد؛
  • خطوط ۳۶–۳۷: امکان инициализация کردن خاصیت [self.__logger]. هنگامی که از متد [calculate_tax] استفاده می‌شود، کلید [logger] بخشی از دیکشنری [config] است؛
  • خط ۳۹: سند متنی مرتبط با پاسخ سرور HTTP ثبت می‌شود؛

لایه [dao] قرار است به‌طور همزمان توسط چندین نخ اجرا شود. با این حال، در اینجا تنها یک نمونه از این لایه ایجاد می‌شود (رجوع شود به config_layers). بنابراین باید بررسی کنیم که کد شامل دسترسی نوشتن به داده‌های مشترک، معمولاً ویژگی‌های کلاس [ImpôtsDaoWithHttpClient] که لایه [dao] را پیاده‌سازی می‌کند، نباشد. با این حال، در کد بالا، خط ۳۷ یک ویژگی از نمونه کلاس را تغییر می‌دهد. در این مورد، این موضوع عواقبی ندارد زیرا همهٔ نخ‌ها از یک لاگر مشترک استفاده می‌کنند. اگر اینطور نبود، دسترسی به خط ۳۷ باید همگام‌سازی می‌شد.

24.4.3. اسکریپت اصلی

اسکریپت اصلی [main] به شرح زیر تغییر می‌کند:


# پیکربندی برنامه

import config
config = config.configure({})

#وابستگی‌ها
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


#اجرای لایه [dao] در یک نخ
# 'مالیات‌دهندگان' فهرستی از مالیات‌دهندگان است
def thread_function(dao, logger, taxpayers: list):
    


# فهرست نخ‌های مشتری
threads = []
logger = None
# کد
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
    # ما آن را در پیکربندی ذخیره می‌کنیم
    config["logger"] = logger
    # بازیابی لایه [dao]
    dao = config["layers"]["dao"]
    # خواندن داده‌های مالیات‌دهنده
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # مودی‌ها؟
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #محاسبه مالیات مودیان با استفاده از چند نخ
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #هر نخ بین ۱ تا ۴ مودی را پردازش خواهد کرد
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        # فهرست مالیات‌دهندگان پردازش‌شده توسط نخ
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # افزایش i برای نخ بعدی
        i += nb_taxpayers
        # رشته ایجاد می‌شود
        thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
        # به فهرست نخ‌های اسکریپت اصلی اضافه می‌شود
        threads.append(thread)
        # رشته راه‌اندازی می‌شود – این عملیات غیرهم‌زمان است – نتیجهٔ رشته منتظر نمی‌ماند
        thread.start()
    # رشتهٔ اصلی منتظر تمام رشته‌هایی که راه‌اندازی کرده تا پایان کارشان می‌ماند
    for thread in threads:
        thread.join()
    #در اینجا، همهٔ نخ‌ها کار خود را به پایان رسانده‌اند – هر یک یک یا چند شیء را تغییر داده‌اند [taxpayer]
    #نتایج در فایل jSON ذخیره می‌شوند
    dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
    #نمایش خطا
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #لاگر بسته شده است
    if logger:
        logger.close()
    #کار ما تمام شد
    print("Travail terminé...")
    # پایان دادن به هر نخ (thread) که ممکن است هنوز در حال اجرا باشد اگر فرآیند به دلیل خطا متوقف شده باشد
    sys.exit()
  • اسکریپت اصلی با اسکریپت کلاینت قبلی در این است که چندین نخ اجرایی برای ارسال درخواست‌ها به سرور ایجاد می‌کند. کلاینت در نسخه ۶ تمام درخواست‌های خود را به صورت متوالی ارسال می‌کرد. درخواست شماره i تنها پس از دریافت پاسخ درخواست شماره [i-1] انجام شد. در اینجا، می‌خواهیم ببینیم که سرور هنگام دریافت چندین درخواست همزمان چگونه رفتار می‌کند. برای این کار، به نخ‌ها (threads) نیاز داریم؛
  • خط ۲۱: رشته‌های تولید شده در یک لیست قرار داده می‌شوند. مهم است که درک کنیم اسکریپت [main] خود توسط رشته‌ای به نام [MainThread] اجرا می‌شود. این رشته اصلی، رشته‌های دیگری را ایجاد خواهد کرد که مسئول محاسبه مالیات برای یک یا چند مؤدی هستند؛
  • خط ۲۶: یک لاگر ایجاد می‌شود. این لاگر توسط همه رشته‌ها به اشتراک گذاشته می‌شود؛
  • خط ۳۲: تمام مودیانی که باید مالیات آن‌ها محاسبه شود، بازیابی می‌شوند؛
  • خطوط ۳۹–۵۱: این مودیان مالیاتی در چندین نخ توزیع می‌شوند؛
  • خطوط ۴۰–۴۱: هر نخ بین ۱ تا ۴ مؤدی را پردازش خواهد کرد. این عدد به صورت تصادفی تعیین می‌شود؛
    • [random.randint(1, 4)] به‌طور تصادفی عددی را از لیست [1, 2, 3, 4] انتخاب می‌کند؛
    • یک رشته نمی‌تواند بیش از [l-i] مودی داشته باشد، که در آن [l-i] نشان‌دهنده تعداد مودی‌هایی است که هنوز رشته‌ای به آنها اختصاص داده نشده است؛
    • بنابراین حداقل این دو مقدار را انتخاب می‌کنیم؛
  • خط ۴۳: به محض اینکه [nb_taxpayers]، تعداد مالیات‌دهندگانی که توسط نخ پردازش شده‌اند، مشخص شود، این‌ها از فهرست مالیات‌دهندگان برداشته می‌شوند:
    • [slice(10,12)] مجموعهٔ شاخص‌های [10, 11, 12] است؛
    • [response.text[39:]] فهرست [taxpayers[10], taxpayers[11], taxpayers[12];
  • خط ۴۵: مقدار i که حلقه در خط ۳۹ را کنترل می‌کند، افزایش می‌یابد؛
  • خط ۴۷: یک نخ ایجاد می‌شود:
    • [target=thread_function] تابع را مشخص می‌کند که باید توسط نخ اجرا شود. این تابع در خطوط 16–17 است. این تابع سه پارامتر می‌گیرد؛
    • [ags] فهرست سه پارامتر مورد انتظار تابع [thread_function] است؛

ایجاد یک نخ، آن را اجرا نمی‌کند. این کار صرفاً یک شیء ایجاد می‌کند؛

  • خطوط ۴۸–۴۹: رشته‌ای که به تازگی ایجاد شده است به لیست رشته‌های ایجاد شده توسط رشته اصلی اضافه می‌شود؛
  • خط ۵۱: نخ اجرا می‌شود. سپس به صورت موازی با نخ‌های فعال دیگر اجرا خواهد شد. در اینجا، تابع [thread_function] را با آرگومان‌های ارائه‌شده اجرا خواهد کرد؛
  • خطوط ۵۳–۵۴: نخ اصلی منتظر هر یک از نخ‌هایی که راه‌اندازی کرده است می‌ماند. بیایید یک مثال بزنیم:
    • رشته اصلی سه رشته [th1, th2, th3] را راه‌اندازی کرده است؛
    • رشته اصلی در انتظار هر یک از رشته‌ها (خطوط ۵۳–۵۴) به ترتیب حلقه for می‌ماند: [th1, th2, th3];
    • فرض کنیم نخ‌ها به ترتیب [th2, th1, th3] پایان می‌یابند؛
    • رشته اصلی منتظر پایان th1 می‌ماند. وقتی th2 تمام می‌شود، هیچ اتفاقی نمی‌افتد؛
    • وقتی th1 تمام می‌شود، نخ اصلی منتظر th2 می‌ماند. اما th2 قبلاً تمام شده است. سپس نخ اصلی به نخ بعدی رفته و منتظر th3 می‌ماند؛
    • وقتی th3 تمام می‌شود، نخ اصلی منتظر ماندن را متوقف کرده و به اجرای خط ۵۷ می‌پردازد؛
  • خط ۵۷ نتایج را در فایل نتایج می‌نویسد. این یک مثال خوب از ارجاعات شیء است:
    • خط ۴۳: لیست [thread_payers] که با یک تِرد مرتبط است، شامل نسخه‌هایی از ارجاعات شیء موجود در لیست [taxpayers] است؛
    • ما می‌دانیم که محاسبه مالیات اشیایی را که توسط ارجاعات در لیست [thread_payers] به آنها اشاره شده است، تغییر خواهد داد. این اشیاء با نتایج محاسبه مالیات به‌روزرسانی خواهند شد. با این حال، خود ارجاعات تغییر نمی‌کنند. بنابراین، ارجاعات در لیست اولیه [taxpayers] به اشیاء اصلاح‌شده «منظور دارند» یا «اشاره می‌کنند»؛

تابع [thread_function] که توسط نخ‌ها اجرا می‌شود به شرح زیر است:


#اجرای لایه [dao] در یک نخ
# «مالیات‌دهندگان» فهرستی از مالیات‌دهندگان است
def thread_function(dao, logger, taxpayers: list):
    # لاگ شروع نخ
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    # محاسبه مالیات مودیان
    for taxpayer in taxpayers:
        # لاگ
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        #محاسبه همزمان مالیات
        dao.calculate_tax(taxpayer)
        # لاگ
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    #پایان لاگ رشته
    logger.write(f"fin du thread [{thread_name}]\n")
  • نوشتن توابعی که به طور همزمان توسط چندین نخ اجرا می‌شوند، اغلب دشوار است: باید همیشه بررسی کرد که کد تلاش نکند داده‌های مشترک بین نخ‌ها را تغییر دهد. هنگامی که این اتفاق می‌افتد، دسترسی همگام برای داده‌های مشترکی که قرار است تغییر یابند، باید پیاده‌سازی شود؛
  • خط ۳: این تابع سه پارامتر می‌گیرد:
    • [dao]: اشاره‌ای به لایه [dao]. این داده مشترک است؛
    • [logger]: مرجعی به لاگر. این داده مشترک است؛
    • [taxpayers]: فهرستی از مالیات‌دهندگان. این داده مشترک نیست: هر نخ (thread) یک فهرست متفاوت را مدیریت می‌کند؛
  • بیایید دو مرجع [dao, logger] را بررسی کنیم:
    • ما دیده‌ایم که شیء اشاره شده توسط مرجع [dao]، یک مرجع [self.__logger] داشت که توسط رشته‌ها (threads) تغییر می‌کرد، اما فقط برای تنظیم آن به یک مقدار مشترک برای همه رشته‌ها؛
    • اشاره [logger] به یک توصیفگر فایل اشاره می‌کند. ما دیده‌ایم که هنگام نوشتن لاگ‌ها در فایل ممکن است مشکلی وجود داشته باشد. به همین دلیل، نوشتن در فایل همگام‌سازی شده است؛
  • خطوط ۵–۶: نام نخ و تعداد مالیات‌دهندگان تحت مدیریت آن ثبت می‌شود؛
  • خطوط ۸–۱۴: محاسبه مالیات مودیان؛
  • خط ۱۶: پایان نخ ثبت می‌شود؛

24.4.4. اجرا

وب‌سرور را همان‌طور که در بخش قبلی (وب‌سرور، SGBD، hMailServer، Thunderbird) توضیح داده شد، راه‌اندازی کنید، سپس اسکریپت کلاینت [main] را اجرا کنید. در فایل‌های [data/output/errors.txt, data/output/résultats.json همان نتایج نسخه قبلی را مشاهده می‌کنیم. در فایل [data/logs/logs.txt]، لاگ‌های زیر را مشاهده می‌کنیم:


2020-07-24 10:05:20.942404, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-24 10:05:20.943458, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-24 10:05:20.943458, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-24 10:05:20.946502, Thread-3 : début du thread [Thread-3] avec 1 contribuable(s)
2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-24 10:05:20.976143, 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-24 10:05:20.976695, 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-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
2020-07-24 10:05:21.973914, 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-24 10:05:21.973914, 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-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-24 10:05:21.977130, 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-24 10:05:21.977130, 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-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-24 10:05:21.982634, Thread-3 : {"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-24 10:05:21.982634, 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-24 10:05:21.983134, Thread-3 : 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-24 10:05:21.983134, 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-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-24 10:05:22.008562, 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-24 10:05:22.008562, 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-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-24 10:05:22.016848, Thread-5 : {"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-24 10:05:22.017349, Thread-5 : 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-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
2020-07-24 10:05:23.008486, Thread-2 : {"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-24 10:05:23.008486, Thread-2 : 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-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-24 10:05:23.011722, 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-24 10:05:23.013723, 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-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-24 10:05:23.024135, Thread-2 : {"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-24 10:05:23.024135, Thread-2 : 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-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
2020-07-24 10:05:23.025178, Thread-4 : {"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-24 10:05:23.026191, Thread-4 : 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-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
  • این لاگ‌ها نشان می‌دهند که پنج تِرد برای محاسبه مالیات ۱۱ مؤدی راه‌اندازی شده‌اند. این پنج تِرد درخواست‌های همزمان را به سرور محاسبه مالیات ارسال کردند. درک نحوه عملکرد این موضوع مهم است:
    • رید [Thread-1] ابتدا راه‌اندازی می‌شود. زمانی که پردازنده (CPU) در اختیار دارد، در کد پیش می‌رود تا اینکه درخواست خود HTTP را ارسال می‌کند. از آنجایی که باید منتظر نتیجه این درخواست بماند، به طور خودکار در حالت تعلیق قرار می‌گیرد. سپس پردازنده را از دست می‌دهد و یک رید دیگر جایگزین آن می‌شود؛
    • خطوط ۱–۱۰: همین فرآیند برای هر پنج نخ تکرار می‌شود. بنابراین، هر پنج نخ قبل از اینکه نخ [Thread-1] حتی پاسخ خود را در خط ۱۱ دریافت کند، راه‌اندازی می‌شوند؛
  • رشته‌ها به ترتیبی که شروع شده‌اند، به پایان نمی‌رسند. بنابراین، رشته‌ی [Thread-3] است که ابتدا در خط ۲۳ به پایان می‌رسد؛

در سمت سرور، لاگ‌های موجود در فایل [data/logs/logs.txt] به شرح زیر است:


2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=2&salary=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=3&salary=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=2&salary=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [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-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?married=no&children=3&salary=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=2&salary=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [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-24 10:05:21.973914, Thread-3 : [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-24 10:05:21.977130, Thread-6 : [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-24 10:05:21.977130, Thread-5 : [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-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=3&salary=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=5&salary=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?married=no&children=0&salary=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [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-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?married=yes&children=3&salary=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [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-24 10:05:23.003533, Thread-7 : [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-24 10:05:23.006434, Thread-8 : [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}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?married=no&children=2&salary=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [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-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?married=no&children=0&salary=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [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}}}
  • می‌توانیم ببینیم که ۱۱ نخ به ۱۱ مالیات‌دهنده رسیدگی کرده‌اند؛
  • برخی از نخ‌ها در حالت تعلیق قرار گرفتند (خطوط 6، 8، 12، 14، 20، 22) در حالی که برخی دیگر قرار نگرفتند (خطوط 9، 23، 25، 29، 31)؛

24.5. آزمایش‌های لایه [dao]

همان‌طور که در |نسخه قبلی| انجام دادیم، در حال آزمایش لایه [dao] مشتری هستیم. اصل دقیقاً یکسان است:

Image

کلاس تست در محیط زیر اجرا خواهد شد:

Image

  • پیکربندی [2] با پیکربندی [1] که همین حالا بررسی کردیم، یکسان است؛

کلاس تست [TestHttpClientDao] به شرح زیر است:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'married': 'yes', 'children': 2, 'salary': 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 = config["layers"]["dao"]

    # روش‌های آزمون را اجرا کنید
    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/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s

OK

Process finished with exit code 0