Skip to content

24. تمرين عملي: الإصدار 7

24.1. مقدمة

الإصدار 7 من تطبيق حساب الضرائب مطابق للإصدار 6 باستثناء التفاصيل التالية:

  • سيقوم عميل الويب بإرسال عدة طلبات HTTP في وقت واحد. في الإصدار السابق، كانت هذه الطلبات تُرسل بالتتابع. وبالتالي، كان بإمكان الخادم معالجة طلب واحد فقط في كل مرة؛
  • سيكون الخادم متعدد الخيوط: سيكون قادرًا على معالجة طلبات متعددة في وقت واحد؛
  • لتتبع تنفيذ هذه الطلبات، سيتم تزويد خادم الويب بمسجل يقوم بتسجيل اللحظات الرئيسية في معالجة الطلبات في ملف نصي؛
  • سيقوم الخادم بإرسال بريد إلكتروني إلى مسؤول التطبيق عندما يواجه مشكلة تمنعه من البدء، وعادةً ما تكون هذه المشكلة متعلقة بقاعدة البيانات المرتبطة بخادم الويب؛

تظل بنية التطبيق دون تغيير:

Image

هيكل دليل البرامج النصية كما يلي:

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:
    #  class attribute
    verrou = threading.RLock()

    #  manufacturer
    def __init__(self, logs_filename: str):
        try:
            #  open the file in append mode (a)
            self.__resource = codecs.open(logs_filename, "a", "utf-8")
        except BaseException as erreur:
            raise ImpôtsError(18, f"{erreur}")

    #  writing a log
    def write(self, message: str):
        #  current date / time
        today = date.today()
        now = datetime.time(datetime.now())
        #  thread name
        thread_name = current_thread().name
        #  you don't want to be disturbed while writing to the log file
        #  we request the class's synchronization object (= lock) - only one thread will get it
        Logger.verrou.acquire()
        try:
            #  log entry
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            #  write immediately - otherwise the text will only be written when the write stream is closed
            #  we want to track logs over time
            self.__resource.flush()
        finally:
            #  release the synchronization object (= lock) so that another thread can obtain it
            Logger.verrou.release()

    #  freeing up resources
    def close(self):
        #  close file
        if self.__resource:
            self.__resource.close()
  • السطران 10-11: نُعرِّف سمة فئة. سمة الفئة هي خاصية مشتركة بين جميع مثيلات الفئة. ويُشار إليها باستخدام الترميز [Class.class_attribute] (السطران 30 و39). وستعمل سمة الفئة [lock] ككائن تزامن لجميع الخيوط التي تُنفِّذ الكود الوارد في الأسطر 31-36؛
  • الأسطر 14-19: يتلقى المنشئ المسار المطلق لملف السجل. ثم يتم فتح هذا الملف، ويتم تخزين واصف الملف الذي تم استرداده في الفئة؛
  • السطر 17: يتم فتح ملف السجل في وضع "الإضافة" (a). سيتم إضافة كل سطر مكتوب إلى نهاية الملف؛
  • الأسطر 22-39: تسمح طريقة [write] بكتابة رسالة تم تمريرها كمعلمة إلى ملف السجل. يتم إلحاق معلومتين بهذه الرسالة:
    • السطر 24: التاريخ الحالي؛
    • السطر 25: الوقت الحالي؛
    • السطر 27: اسم الخيط الذي يكتب السجل. من المهم أن نتذكر هنا أن تطبيق الويب يخدم عدة مستخدمين في وقت واحد. يتم تعيين خيط لكل طلب لتنفيذه. إذا تم إيقاف هذا الخيط مؤقتًا — عادةً لعملية إدخال/إخراج (الشبكة، الملفات، قاعدة البيانات) — يتم تسليم المعالج إلى خيط آخر. بسبب هذه الانقطاعات المحتملة، لا يمكننا التأكد من أن الخيط سينجح في كتابة سطر في ملف السجل دون أن يتعرض للانقطاع. لذلك، هناك خطر من اختلاط السجلات من خيطين مختلفين. هذا الخطر منخفض، وربما يكون معدومًا، لكننا قررنا مع ذلك إظهار كيفية مزامنة وصول خيطين إلى مورد مشترك، وهو في هذه الحالة ملف السجل؛
  • السطر 30: قبل الكتابة، يطلب الخيط مفتاح باب الدخول. المفتاح المطلوب هو الذي تم إنشاؤه في السطر 11. وهو بالفعل فريد: سمة الفئة فريدة لجميع مثيلات الفئة؛
    • في الوقت T1، يحصل مؤشر ترابط باسم Thread1 على المفتاح. يمكنه بعد ذلك تنفيذ السطر 33؛
    • في الوقت T2، يتم إيقاف مؤقتًا مؤشر الترابط Thread1 قبل أن ينتهي حتى من كتابة السجل؛
    • في الوقت T3، يجب على الخيط Thread2، الذي حصل على المعالج، كتابة سجل أيضًا. وبذلك يصل إلى السطر 30، حيث يطلب مفتاح الباب الأمامي. ويتم إخباره أن خيطًا آخر يمتلكه بالفعل. ثم يتم إيقافه مؤقتًا تلقائيًا. وسيكون هذا هو الحال بالنسبة لجميع الخيوط التي تطلب هذا المفتاح؛
    • في الوقت T4، يستعيد الخيط Thread1، الذي كان متوقفًا مؤقتًا، المعالج. ثم ينهي كتابة السجل؛
  • الأسطر 32-36: تتم الكتابة إلى ملف السجل على خطوتين:
  • السطر 33: يعمل واصف الملف الذي تم الحصول عليه في السطر 17 مع مخزن مؤقت. تكتب عملية [الكتابة] في السطر 33 إلى هذا المخزن المؤقت وليس مباشرة إلى الملف. ثم يتم تفريغ المخزن المؤقت إلى الملف في ظل ظروف معينة:
        • عندما يمتلئ المخزن المؤقت؛
        • يخضع واصف الملف لعملية [close] أو [flush]؛
  • السطر 36: نجبر سطر السجل على الكتابة إلى الملف. نقوم بذلك لأننا نريد رؤية السجلات من الخيوط المختلفة متداخلة. إذا لم نقم بذلك، فسيتم كتابة السجلات من خيط واحد في نفس الوقت — —عندما يتم إغلاق الوصف في السطر 45. سيكون من الصعب بعد ذلك رؤية أن خيوطًا معينة قد توقفت: سيتعين علينا التحقق من الطوابع الزمنية في السجلات؛
  • السطر 39: يعيد مؤشر الترابط Thread1 القفل الذي تم منحه له. يمكن الآن منحه لمؤشر ترابط آخر؛
  • السطر 22: وبالتالي، يتم مزامنة طريقة [write]: حيث يكتب مؤشر ترابط واحد فقط في كل مرة إلى ملف السجل. ويكمن مفتاح هذه الآلية في السطر 30: بغض النظر عما يحدث، لا يسترد سوى مؤشر ترابط واحد المفتاح للانتقال إلى السطر التالي. ويحتفظ به حتى يعيده (السطر 39)؛
  • الأسطر 41–45: تطلق الطريقة [close] الموارد المخصصة لموصف ملف السجل؛

ستبدو السجلات المكتوبة في ملف السجل كما يلي:

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

24.2.2. فئة [SendAdminMail]

تسمح لك فئة [SendAdminMail] بإرسال رسالة إلى مسؤول التطبيق عند تعطل التطبيق.

Image

يتم تكوين فئة [SendAdminMail] في البرنامج النصي [config] [2] على النحو التالي:

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        }

تستقبل فئة [SendAdminMail] القاموس من الأسطر 2–13 بالإضافة إلى تكوين إرسال البريد الإلكتروني. الفئة هي كما يلي:

#  imports
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):
        #  sends message to smtp server config['smtp-server'] on port config[smtp-port]
        #  if config['tls'] is true, TLS support will be used
        #  mail is sent from config['from']
        #  for recipient config['to']
        #  message has subject config['subject']
        #  a logger reference can be found in config['logger']

        #  retrieve logger from config - can be None
        logger = config["logger"]
        #  server SMTP
        server = None
        #  we send the message
        try:
            #  the SMTP server
            server = smtplib.SMTP(config["smtp-server"])
            #  verbose mode
            server.set_debuglevel(verbose)
            #  secure connection?
            if config['tls']:
                #  start of safety dialogue
                server.starttls()
                #  authentication
                server.login(config["user"], config["password"])
            #  construction of a Multipart message - this is the message that will be sent
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            #  we send the message
            server.send_message(msg)
            #  log - the logger may not exist
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            #  log- the logger may not exist
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            #  we're done - we release the resources mobilized by the function
            if server:
                server.quit()
  • الأسطر 24-54: هذا هو الكود الذي تمت تغطيته بالفعل في المثال |smtp/02
  • السطر 20: نسترد مرجع مسجل. ويُستخدم هذا في السطرين 45 و49؛

24.3. خادم الويب

24.3.1. التكوين

تشبه تكوينات الخادم إلى حد كبير تلك التي تمت مناقشتها سابقًا. لم يتغير سوى ملف [config.py] بشكل طفيف:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  IndexController
        f"{root_dir}/impots/http-servers/01/controllers",
        #  scripts [config_database, config_layers]
        script_dir,
        #  Logger, SendAdminMail
        f"{script_dir}/../utilities",
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },
        #  thread pause time in seconds
        "sleep_time": 0
    })

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • الأسطر 40–66: نضيف العناصر المتعلقة بمسجل الأحداث (السطر 49) وتلك المتعلقة بإرسال بريد إلكتروني تنبيهي إلى مسؤول التطبيق (الأسطر 51–63) إلى قاموس تكوين الخادم؛
  • السطر 65: لرؤية الخيوط أثناء العمل بشكل أفضل، سنقوم بإجبار بعضها على التوقف مؤقتًا. [sleep_time] هي مدة التوقف المؤقت معبراً عنها بالثواني؛
  • الأسطر 27–28: لاحظ أننا نستخدم [index_controller] من الإصدار 6 السابق؛

24.3.2. البرنامج النصي الرئيسي [main]

النص البرمجي الرئيسي [main] هو كما يلي:

#  a mysql or pgres parameter is expected
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()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
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

#  authentication manager
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    #  user list
    users = config['users']
    #  browse this list
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    #  we didn't find
    return False


#  send an e-mail to the administrator
def send_adminmail(config: dict, message: str):
    #  send an e-mail to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

#  start-up log
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  the Flask application can be started
app = Flask(__name__)


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


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • الأسطر 1-10: يتوقع البرنامج النصي معلمة [mysql / pgres] تحدد نظام إدارة قواعد البيانات (DBMS) المطلوب استخدامه؛
  • الأسطر 12–14: يتم تكوين التطبيق (مسار Python، الطبقات، قاعدة البيانات)؛
  • الأسطر 16–28: التبعيات المطلوبة من قبل التطبيق؛
  • الأسطر 30-43: إدارة المصادقة؛
  • الأسطر 46–51: دالة ترسل بريدًا إلكترونيًا إلى مسؤول التطبيق؛
  • تتوقع الدالة معلمتين:
      • config: قاموس يحتوي على المفاتيح [adminMail] و [logger]؛
      • الرسالة المراد إرسالها؛
    • السطور 49-50: نقوم بإعداد تكوين البريد الإلكتروني؛
    • نرسل البريد الإلكتروني؛
  • الأسطر 54–74: نتحقق من وجود ملف السجل؛
  • الأسطر 70–74: إذا تعذر فتح ملف السجل، نرسل بريدًا إلكترونيًا إلى المسؤول ونخرج؛
  • الأسطر 76–79: تسجيل بدء تشغيل الخادم؛
  • الأسطر 81–98: نسترد بيانات إدارة الضرائب من قاعدة البيانات؛
  • الأسطر 88–98: إذا تعذر استرداد هذه البيانات، نقوم بتسجيل الخطأ في كل من وحدة التحكم وملف السجل؛
  • الأسطر 100–101: لن يقوم الخيط الرئيسي بالتسجيل بعد الآن (لن تستخدم الخيوط التي تم إنشاؤها نفس واصف الملف)؛
  • الأسطر 103–105: إذا لم نتمكن من الاتصال بقاعدة البيانات، نتوقف؛
  • السطر 122: يتم تشغيل الخادم في الوضع متعدد الخيوط؛

وظيفة [index] (السطر 114) هي كما يلي:

#  Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[index] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  the request is executed by a controller
        résultat, status_code = index_controller.execute(request, config)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        #  we log the answer
        logger.write(f"[index] {résultat}\n")
        #  we send the answer
        return json_response(résultat, status_code)
    except BaseException as erreur:
        #  log the error if possible
        if logger:
            logger.write(f"[index] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send the answer
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • السطر 4: الدالة التي يتم تنفيذها عندما يطلب المستخدم عنوان URL /. نظرًا لأن الخادم متعدد الخيوط (السطر 112)، سيتم إنشاء خيط لتنفيذ الدالة. يمكن مقاطعة هذا الخيط وإيقافه مؤقتًا في أي وقت لاستئناف التنفيذ بعد ذلك بقليل. ضع هذا في اعتبارك دائمًا عندما يصل الكود إلى مورد مشترك بين جميع الخيوط. في هذه الحالة، هذا المورد هو ملف السجل: تكتب جميع الخيوط إليه؛
  • السطر 8: نقوم بإنشاء مثيل لـ logger. وبالتالي، سيكون لكل مؤشر ترابط مثيل مختلف لـ logger. ومع ذلك، تشير جميع أدوات التسجيل هذه إلى ملف السجل نفسه. لا يزال من المهم ملاحظة أنه عندما يغلق مؤشر ترابط ما أداة التسجيل الخاصة به، فإن هذا لا يؤثر على أدوات التسجيل الخاصة بمؤشرات الترابط الأخرى؛
  • الأسطر 9–12: نقوم بتخزين المسجل في قاموس [config] الخاص بالتطبيق تحت مفتاح يحمل اسم الخيط. وبالتالي، إذا كان هناك n خيوط تعمل في وقت واحد، فسيتم إنشاء n إدخالات في قاموس [config]. [config] هو مورد مشترك بين جميع الخيوط. لذلك، قد تكون هناك حاجة إلى التزامن. لقد قمت بافتراض هنا. افترضت أنه إذا أنشأ خيطان في نفس الوقت إدخالاتهما في ملف [config] وتوقف أحدهما بسبب الآخر، فلن يكون لذلك أي تأثير. يمكن للخيط الذي توقف أن يكمل إنشاء الإدخال لاحقًا. إذا أظهر الاختبار أن هذا الافتراض خاطئ، فسيكون من الضروري مزامنة الوصول إلى السطر 12؛
  • السطر 10: نضع المسجل في قاموس؛
  • السطر 11: [threading.current_thread()] هو الخيط الذي ينفذ هذا السطر، وبالتالي الخيط الذي ينفذ وظيفة [index]. نسجل اسمه. لكل خيط اسم فريد؛
  • السطر 12: نقوم بتخزين تكوين الخيط. من الآن فصاعدًا، سنقوم دائمًا بما يلي: إذا كانت هناك معلومات لا يمكن مشاركتها بين الخيوط، فسيتم وضعها في التكوين العام، ولكن مرتبطة باسم الخيط؛
  • السطر 14: نقوم بتسجيل الطلب الذي نقوم بتنفيذه حاليًا؛
  • الأسطر 15-24: نقوم بإيقاف مؤقت لبعض الخيوط بشكل عشوائي حتى تفسح المجال للمعالج لخيوط أخرى؛
    • السطر 16: نستخرج مدة التوقف (بالثواني) من ملف التكوين؛
    • السطر 17: لا تحدث وقفة إلا إذا كانت مدة الوقفة غير صفر؛
    • السطر 19: عدد صحيح عشوائي في النطاق [0، 1]. لذلك، لا يمكن أن تكون القيم إلا 0 و 1؛
    • السطر 20: يتم إيقاف مؤقتًا مؤشر الترابط فقط إذا كان الرقم العشوائي هو 1؛
    • السطر 22: نسجل حقيقة أن الخيط على وشك التوقف مؤقتًا؛
    • السطر 24: يتم إيقاف مؤقتًا الخيط لمدة [sleep_time] ثانية؛
  • السطر 26: عندما يستيقظ الخيط، يقوم الوحدة النمطية [index_controller] بتنفيذ الطلب؛
  • الأسطر 28–32: إذا تسبب هذا التنفيذ في ظهور [500 INTERNAL SERVER ERROR]، يتم إرسال بريد إلكتروني إلى المسؤول؛
    • السطران 30-31: نقوم بتكوين قاموس [config_mail] الذي سنمرره إلى فئة [SendAdminMail]؛
    • السطر 32: الرسالة المرسلة إلى المسؤول هي سلسلة JSON للنتيجة التي سيتم إرسالها إلى العميل؛
  • الأسطر 33–34: نقوم بتسجيل الاستجابة التي سيتم إرسالها إلى العميل (السطر 36)؛
  • الأسطر 37-44: معالجة أي استثناءات؛
  • السطران 39-40: إذا كان المسجل موجودًا، نقوم بتسجيل الخطأ الذي حدث؛
  • السطران 47-48: نغلق المسجل إذا كان موجودًا. في النهاية، يقوم الخيط بإنشاء مسجل في بداية الطلب ويغلقه بمجرد معالجة الطلب؛

24.3.3. وحدة التحكم [index_controller]

وحدة التحكم [index_controller] التي تنفذ الطلبات هي نفسها الموجودة في الإصدار السابق:

Image

24.3.4. التنفيذ

نقوم بتشغيل خادم Flask وخادم البريد الإلكتروني |hMailServer| وعميل البريد الإلكتروني |Thunderbird|. لا نقوم بتشغيل نظام إدارة قواعد البيانات. يتوقف الخادم مع ظهور سجلات وحدة التحكم التالية:


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

ثم قم بتشغيل نظام إدارة قواعد البيانات (DBMS) واطلب عنوان 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/?marié=oui&enfants=3&salaire=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}}}
  • الأسطر 1-4: لاحظ أن الخادم يبدأ مرتين لأن وضع [Debug=True] يؤدي إلى تشغيل ثانٍ؛
  • السطور 5-6: تمنحنا السجلات فكرة عن وقت تنفيذ الطلب، وهو هنا 2.293 مللي ثانية؛

24.4. عميل الويب

Image

يتم إنشاء الدليل [http-clients/02] عن طريق نسخ الدليل [http-clients/01]. ثم نقوم بإجراء بعض التعديلات.

24.4.1. التكوين

تكوين [config] لتطبيق [http-clients/02] هو نفسه تكوين تطبيق [http-clients/01]، مع بعض الاختلافات الطفيفة:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
        #  Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration with constants
    config.update({
        #  taxpayer file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        #  results file
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        #  error file
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  tax calculation server
        "server": {
            "urlServer": "http://127.0.0.1:5000/",
            "authBasic": True,
            "user": {
                "login": "admin",
                "password": "admin"
            }
        },
        #  debug mode
        "debug": True
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • السطران 31-32: سنستخدم نفس المسجل |Logger| المستخدم للخادم؛
  • السطر 49: المسار المطلق لملف السجل؛
  • السطر 60: يُستخدم الوضع [debug=True] لكتابة استجابات خادم الويب في ملف السجل؛

24.4.2. طبقة [dao]

يتغير كود فئة [ImpôtsDaoWithHttpClient] قليلاً:

#  imports

import requests
from flask_api import status




class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None

    #  unused method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        
        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")
        #  response status code HTTP
        status_code = response.status_code
        
  • السطر 17: نقوم بتخزين التكوين العام. سنرى لاحقًا أنه عند تشغيل منشئ فئة [ImpôtsDaoWithHttpClient]، لا يحتوي قاموس [config] بعد على المفتاح [logger] المستخدم في السطر 37. ولهذا السبب لا يمكننا تهيئة [self.__logger] (السطر 23) في المنشئ؛
  • السطر 21: أضفنا مفتاح [debug] إلى التكوين الذي يتحكم في التسجيل في الأسطر 33-39؛
  • السطر 34: إذا كنا في وضع [debug]؛
  • السطور 36-37: تهيئة اختيارية لخاصية [self.__logger]. عند استخدام طريقة [calculate_tax]، يكون المفتاح [logger] جزءًا من القاموس [config]؛
  • السطر 39: نقوم بتسجيل المستند النصي المرتبط باستجابة HTTP للخادم؛

سيتم تنفيذ طبقة [dao] في وقت واحد بواسطة خيوط متعددة. ومع ذلك، نقوم هنا بإنشاء مثيل واحد لهذه الطبقة (انظر config_layers). لذلك يجب علينا التحقق من أن الكود لا يتضمن وصولًا للكتابة إلى البيانات المشتركة، وعادةً ما تكون خصائص فئة [ImpôtsDaoWithHttpClient] التي تنفذ طبقة [dao]. ومع ذلك، في الكود أعلاه، يقوم السطر 37 بتعديل خاصية لمثيل الفئة. وهنا، لا يترتب على ذلك أي عواقب لأن جميع الخيوط تشترك في نفس المسجل. ولو لم يكن الأمر كذلك، لكان من الضروري مزامنة الوصول إلى السطر 37.

24.4.3. النص البرمجي الرئيسي

يتطور البرنامج النصي الرئيسي [main] على النحو التالي:

#  configure the application

import config
config = config.configure({})

#  dependencies
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    


#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  multi-threaded tax calculation for taxpayers
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #  each thread will process from 1 to 4 contributors
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        #  the list of taxpayers processed by the thread
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        #  increment i for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    for thread in threads:
        thread.join()
    #  here all threads have finished their work - each has modified one or more objects [taxpayer]
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • يختلف البرنامج النصي الرئيسي عن البرنامج النصي للعميل السابق في أنه سيقوم بإنشاء خيوط تنفيذ متعددة لإرسال الطلبات إلى الخادم. كان العميل في الإصدار 6 يرسل جميع طلباته بالتسلسل. لم يتم إرسال الطلب رقم #i إلا بعد استلام الرد على الطلب رقم #[i-1]. هنا، نريد أن نرى كيف يتصرف الخادم عند استلامه لطلبات متعددة في وقت واحد. ولهذا، نحتاج إلى خيوط؛
  • السطر 21: سيتم وضع الخيوط التي تم إنشاؤها في قائمة. من المهم أن نفهم أن البرنامج النصي [main] يتم تنفيذه أيضًا بواسطة خيط يسمى [MainThread]. سيقوم هذا الخيط الرئيسي بإنشاء خيوط أخرى ستكون مسؤولة عن حساب الضريبة لواحد أو أكثر من دافعي الضرائب؛
  • السطر 26: نقوم بإنشاء مسجل. سيتم مشاركة هذا المسجل بين جميع الخيوط؛
  • السطر 32: نسترد جميع دافعي الضرائب الذين يجب حساب ضرائبهم؛
  • الأسطر 39-51: نقوم بتوزيع هؤلاء دافعي الضرائب على عدة خيوط؛
  • السطران 40-41: سيقوم كل مؤشر ترابط بمعالجة ما بين 1 إلى 4 دافعي ضرائب. يتم تحديد هذا العدد عشوائياً؛
    • [random.randint(1, 4)] يولد رقمًا عشوائيًا من القائمة [1, 2, 3, 4]؛
    • لا يمكن أن يضم الموضوع أكثر من [l-i] دافع ضرائب، حيث يمثل [l-i] عدد دافعي الضرائب الذين لم يتم تخصيص موضوع لهم بعد؛
    • لذلك نأخذ الحد الأدنى من القيمتين؛
  • السطر 43: بمجرد معرفة [nb_taxpayers]، وهو عدد دافعي الضرائب الذين تمت معالجتهم بواسطة الخيط، نأخذ هؤلاء من قائمة دافعي الضرائب:
    • [slice(10,12)] هي مجموعة المؤشرات [10، 11، 12]؛
    • [taxpayers[slice(10,12)]] هي القائمة [taxpayers[10], taxpayers[11], taxpayers[12] ;
  • السطر 45: نزيد قيمة i، التي تتحكم في الحلقة في السطر 39؛
  • السطر 47: نقوم بإنشاء مؤشر ترابط:
    • [target=thread_function] يحدد الدالة التي سيقوم الخيط بتنفيذها. هذه هي الدالة الموجودة في السطرين 16–17. وهي تتوقع ثلاثة معلمات؛
    • [ags] هي قائمة المعلمات الثلاثة التي تتوقعها الدالة [thread_function]؛

إنشاء مؤشر ترابط لا يؤدي إلى تنفيذه. إنه ببساطة ينشئ كائنًا؛

  • السطران 48-49: يُضاف الخيط الذي تم إنشاؤه للتو إلى قائمة الخيوط التي أنشأها الخيط الرئيسي؛
  • السطر 51: يتم تشغيل الخيط. وسيعمل بعد ذلك بالتوازي مع الخيوط النشطة الأخرى. وهنا، سيقوم بتنفيذ [thread_function] باستخدام الحجج المقدمة إليه؛
  • السطران 53-54: ينتظر الخيط الرئيسي كل خيط من الخيوط التي أطلقها. لنأخذ مثالاً:
    • أطلق الخيط الرئيسي ثلاثة خيوط [th1، th2، th3]؛
    • ينتظر الخيط الرئيسي كل خيط من الخيوط (السطران 53-54) بترتيب حلقة for: [th1، th2، th3]؛
    • لنفترض أن الخيوط تنتهي بالترتيب [th2، th1، th3]؛
    • ينتظر الخيط الرئيسي انتهاء th1. عندما ينتهي th2، لا يحدث شيء؛
    • عندما ينتهي th1، ينتظر الخيط الرئيسي انتهاء th2. ومع ذلك، فقد انتهى th2 بالفعل. ثم ينتقل الخيط الرئيسي إلى الخيط التالي وينتظر انتهاء th3؛
    • عندما ينتهي th3، يكون الخيط الرئيسي قد انتهى من الانتظار ويشرع في تنفيذ السطر 57؛
  • يكتب السطر 57 النتائج في ملف النتائج. هذا مثال جيد على مراجع الكائنات:
    • السطر 43: تحتوي القائمة [thread_payers] المرتبطة بخيط على نسخ من مراجع الكائنات الموجودة في القائمة [taxpayers]؛
    • نعلم أن حساب الضريبة سيعدل الكائنات التي تشير إليها المراجع في قائمة [thread_payers]. سيتم تحديث هذه الكائنات بنتائج حساب الضريبة. ومع ذلك، لا يتم تعديل المراجع نفسها. لذلك، فإن المراجع في قائمة [taxpayers] الأولية "ترى" أو "تشير إلى" الكائنات المعدلة؛

وظيفة [thread_function] التي تنفذها الخيوط هي كما يلي:

#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    #  log thread start
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    #  taxpayers' taxes are calculated
    for taxpayer in taxpayers:
        #  log
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        #  synchronous tax calculation
        dao.calculate_tax(taxpayer)
        #  log
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    #  log end of thread
    logger.write(f"fin du thread [{thread_name}]\n")
  • غالبًا ما يكون كتابة الدوال التي يتم تنفيذها في وقت واحد بواسطة خيوط متعددة أمرًا صعبًا: يجب عليك دائمًا التحقق من أن الكود لا يحاول تعديل البيانات المشتركة بين الخيوط. عندما يحدث ذلك، يجب عليك تنفيذ وصول متزامن إلى البيانات المشتركة التي على وشك التعديل؛
  • السطر 3: تتلقى الدالة ثلاثة معلمات:
    • [dao]: مرجع إلى طبقة [dao]. هذه البيانات مشتركة؛
    • [logger]: مرجع إلى المسجل. هذه البيانات مشتركة؛
    • [taxpayers]: قائمة بالدافعين. هذه البيانات غير مشتركة: يدير كل مؤشر ترابط قائمة مختلفة؛
  • دعونا نلقي نظرة على المرجعين [dao] و[logger]:
    • لقد رأينا أن الكائن الذي تشير إليه مرجع [dao] يحتوي على مرجع [self.__logger] تم تعديله بواسطة الخيوط، ولكن لتعيين قيمة مشتركة لجميع الخيوط؛
    • تشير المرجع [logger] إلى واصف ملف. رأينا أنه قد يكون هناك مشكلة عند كتابة السجلات إلى الملف. لهذا السبب، تمت مزامنة الكتابة إلى الملف؛
  • السطور 5-6: نقوم بتسجيل اسم الخيط وعدد دافعي الضرائب الذين يجب أن يديرهم؛
  • الأسطر 8-14: حساب ضرائب دافعي الضرائب؛
  • السطر 16: تسجيل نهاية الخيط؛

24.4.4. التنفيذ

لنبدأ تشغيل خادم الويب كما هو موضح في القسم السابق (خادم الويب، DBMS، hMailServer، Thunderbird)، ثم نقوم بتشغيل البرنامج النصي [main] الخاص بالعميل. في الملفات [data/output/errors.txt، data/output/results.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]
  • تُظهر هذه السجلات أنه تم تشغيل خمسة خيوط لحساب الضرائب لـ 11 دافع ضرائب. أرسلت هذه الخيوط الخمسة طلبات متزامنة إلى خادم حساب الضرائب. من المهم فهم كيفية عمل ذلك:
    • يتم تشغيل الخيط [Thread-1] أولاً. وعندما يكون لديه وحدة المعالجة المركزية (CPU)، فإنه ينفذ الكود حتى يرسل طلب HTTP الخاص به. ونظرًا لأنه يجب أن ينتظر نتيجة هذا الطلب، يتم تعليقه تلقائيًا. ثم يفقد وحدة المعالجة المركزية (CPU)، ويستلمها خيط آخر؛
    • الأسطر 1-10: تتكرر نفس العملية لكل خيط من الخيوط الخمسة. وبالتالي، يتم تشغيل الخيوط الخمسة قبل أن يتلقى الخيط [Thread-1] ردّه في السطر 11؛
  • لا تنتهي الخيوط بالترتيب الذي تم تشغيلها به. وبالتالي، ينتهي الخيط [Thread-3] أولاً، السطر 23؛

على جانب الخادم، تكون السجلات في الملف [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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=non&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=5&salaire=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/?marié=non&enfants=0&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=non&enfants=2&salaire=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/?marié=non&enfants=0&salaire=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}}}
  • يمكننا أن نرى أن 11 خيطًا عالج 11 دافع ضرائب؛
  • تم تعليق بعض الخيوط (الأسطر 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,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        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__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]

    #  test methods are executed
    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