Skip to content

25. تمرين التطبيق: الإصدار 8

25.1. مقدمة

سنقوم بكتابة تطبيق عميل/خادم جديد. الميزة الجديدة للخادم هي أنه سيدير جلسة عمل. بدلاً من وضع بيانات إدارة الضرائب في كائن نطاق [تطبيق]، سنضعها في كائن نطاق [جلسة عمل]. سيؤدي ذلك إلى انخفاض أداء الكود. عندما يمكن مشاركة كائن في وضع القراءة فقط من قبل جميع المستخدمين، يُفضل جعله كائنًا في نطاق [التطبيق] بدلاً من كائن في نطاق [الجلسة]. نكسب على الأقل بعض النطاق الترددي لأن هذا يقلل من حجم ملف تعريف ارتباط الجلسة. لكننا نريد عرض تطبيق عميل/خادم حيث يتبادل العميل والخادم ملف تعريف ارتباط الجلسة.

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

Image

25.2. خادم الويب

هيكل دليل نصوص الخادم هو كما يلي:

Image

يتم إنشاء المجلد [http-servers/03] مبدئيًا عن طريق نسخ المجلد [http-servers/02]. ثم يتم إجراء التعديلات.

25.2.1. التكوين

هي نفسها كما في |الإصدار السابق| مع بعض التغييرات في البرنامج النصي [config]:


# dépendances absolues
    absolute_dependencies = [
        # dossiers du projet
        # 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",
        # Constantes, Tranches
        f"{root_dir}/impots/v05/entities",
        # index_controller
        f"{script_dir}/../controllers",
        # scripts [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • السطر 17: سنعيد كتابة وحدة تحكم لوظيفة [index] التي تتعامل مع عنوان URL
  • السطر 21: نستخدم الأدوات المساعدة من |الإصدار السابق|؛

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

يقدم البرنامج النصي [main] الجديد بعض التغييرات على البرنامج النصي [main] الرئيسي من الإصدار السابق:


# l'application Flask peut démarrer
app = Flask(__name__)
# clé secrète de la session
app.secret_key = os.urandom(12).hex()
  • السطر 4: نقوم بإنشاء مفتاح سري للتطبيق. نحن نعلم أن هذا ضروري لإدارة الجلسة؛

بعد ذلك، لم يعد يتم طلب البيانات الضريبية في كود [main]. تمت إزالة الأسطر التالية:

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

بالإضافة إلى ذلك، تقبل وحدة التحكم [index_controller] معلمة إضافية، وهي جلسة Flask:

1
2
3
4
from flask import request, Flask, session
.        
        #  the request is executed by a controller
        résultat, status_code = index_controller.execute(request, session, config)

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

تدير وحدة التحكم [index_controller] الآن جلسة عمل:

#  import dependencies
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

#  URL set: /?married=xx&children=yy&salary=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
    #  dependencies
    from TaxPayer import TaxPayer

    #  initially no errors
    erreurs = []
    

    #  mistakes?
    if erreurs:
        #  an error response is returned to the client
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    #  no mistakes, we can work
    #  retrieve the config associated with the thread
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    #  execute the query
    réponse = None
    try:
        #  the simplest case - admindata is already in session
        if session.get('client_id') is not None:
            #  retrieve session information
            client_id = session.get('client_id')
            admindata = AdminData().fromdict(session.get('admindata'))
            #  log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
        else:
            #  data recovery from tax authorities
            admindata = config["layers"]["dao"].get_admindata()
            #  admindata session
            session['admindata'] = admindata.asdict()
            #  we give the customer a number and put it in the session
            #  this will allow us to track it in the server logs
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            #  log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        #  tAX CALCULATION
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        #  we return the answer to the customer
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        #  we return the answer to the customer
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • السطر 14: يتلقى وحدة التحكم الجلسة الحالية من عميل الويب؛
  • الأسطر 35–38: إذا كان لدى العميل جلسة عمل، فإنها تحتوي على مفتاحين:
    • [client_id]: معرف العميل (السطر 37)؛
    • [admindata]: بيانات الإدارة الضريبية في شكل قاموس (السطر 38)؛
  • السطر 35: نتحقق مما إذا كانت الجلسة تحتوي على أحد المفتاحين المتوقعين؛
  • الأسطر 42-51: الحالة التي لم يتم فيها تهيئة جلسة عمل العميل بعد؛
    • السطر 43: استرداد بيانات سلطة الضرائب من طبقة [dao]؛
    • السطر 45: يتم وضع هذه البيانات في الجلسة في شكل قاموس؛
    • السطر 48: يتم تعيين رقم عشوائي للعميل. سيختلف هذا الرقم من عميل لآخر؛
    • السطر 49: يتم تخزين هذا الرقم في الجلسة؛
    • السطر 51: نقوم بتسجيل حقيقة أن بيانات سلطة الضرائب تم استردادها بواسطة طبقة [dao]. الوصول إلى طبقة [dao] مكلف بشكل عام. ولهذا السبب يجب أن يكون محدودًا. الفكرة هنا هي استرداد بيانات الضرائب من طبقة [dao] مرة واحدة، وتخزينها في الجلسة، واسترجاعها من هناك خلال الطلبات اللاحقة من نفس العميل. لاحظ أن هذا ليس الحل الأفضل. نظرًا لأن بيانات الضرائب من الإدارة هي نفسها لجميع العملاء، فإنها تنتمي إلى كائن في نطاق التطبيق؛
  • الأسطر 35-40: الحالة التي تم فيها تهيئة جلسة عمل العميل خلال طلب سابق؛
    • السطر 37: استرداد معرف العميل من الجلسة؛
    • السطر 38: نسترد بيانات الضرائب الخاصة بالإدارة من الجلسة؛
    • السطر 40: نسجل حقيقة أن العميل قد حصل على بيانات الضرائب الخاصة بالإدارة من الجلسة؛

25.3. عميل الويب

Image

25.3.1. طبقة [dao]

25.3.1.1. فئة [ImpôtsDaoWithHttpSession]

يتم تنفيذ طبقة [dao] بواسطة فئة [ImpôtsDaoWithHttpSession] التالية:

#  imports

import requests
from flask_api import status
from myutils import decode_flask_session

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpSession(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
        #  cookies
        self.__cookies = 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
        #  get parameters
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        #  connection with Auth Basic authentication?
        if self.__config_server['authBasic']:
            response = requests.get(
                #  URL of the queried server
                self.__config_server['urlServer'],
                #  URL parameters
                params=params,
                #  basic authentication
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            #  connection without Auth Basic authentication
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies
            #  retrieve the session cookie
            session_cookie = response.cookies.get('session')
            #  we decode it to log it
            if session_cookie:
                #  logger
                if not self.__logger:
                    self.__logger = self.__config['logger']
                #  log on
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        #  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
        #  we put the response jSON in a dictionary
        résultat = response.json()
        #  error if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            #  we know that the errors have been associated with the [errors] key in the response
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        #  we know that the result has been associated with the [result] key in the response
        #  modify the input parameter with this result
        taxpayer.fromdict(résultat["réponse"]["result"])
  • السطر 30: ستدير طبقة [dao] قاموسًا لملفات تعريف الارتباط؛
  • السطر 58: الخاصية [response.cookies] هي قاموس يحتوي على ملفات تعريف الارتباط المرسلة من الخادم في رؤوس HTTP [Set-Cookie]؛
  • السطر 59: يتم تخزين ملفات تعريف الارتباط هذه في طبقة [dao]. وسيتم إرسالها مرة أخرى إلى الخادم خلال الطلبات اللاحقة من نفس العميل؛
  • الأسطر 60–68: على الرغم من أن ذلك ليس ضروريًا بالضرورة، فإننا نسترد ملف تعريف الارتباط الخاص بالجلسة. في قائمة ملفات تعريف الارتباط التي يرسلها الخادم، يرتبط ملف تعريف الارتباط الخاص بالجلسة بالمفتاح [session]؛
  • الأسطر 62–68: نقوم بفك تشفير ملف تعريف الارتباط الخاص بالجلسة لتسجيل دخول المستخدم؛
  • السطر 68: سنعود لاحقًا إلى الدالة [decode_flask_session]، التي تقوم بفك تشفير ملف تعريف ارتباط الجلسة؛
  • السطران 52 و57: مع كل طلب من نفس العميل، يتم إرجاع ملفات تعريف الارتباط المرسلة من الخادم إليه. هكذا يتم الحفاظ على جلسة Flask بين العميل والخادم؛

يجب أن نتذكر الآن أن طبقة [dao] سيتم تنفيذها في وقت واحد بواسطة خيوط متعددة. لذلك يجب أن نفحص جميع خصائص مثيل الفئة ونرى ما إذا كان الوصول المتزامن إلى هذه الخصائص يمثل مشكلة. هنا أضفنا خاصية [self.__cookies] في السطر 30. يتم تعديل هذه الخاصية في السطر 59. لذلك لدينا حق الوصول للكتابة إلى البيانات المشتركة بين جميع الخيوط. ومع ذلك، فإن هذا الوصول يشكل مشكلة: كل خيط يمثل عميلًا معينًا له ملف تعريف ارتباط الجلسة الخاص به. في الواقع، يحتوي على معرف عميل فريد (=خيط) لكل عميل. إذا لم نفعل شيئًا، يمكن للخيط T2 الكتابة فوق ملفات تعريف الارتباط الخاصة بالخيط T1.

لقد رأينا بالفعل طريقة للتعامل مع هذه المشكلة: يمكننا إنشاء مفاتيح مختلفة لكل مؤشر ترابط في ملف [config] الذي يتم تمريره كمعلمة إلى المنشئ (السطر 17). على سبيل المثال، يمكننا استخدام اسم مؤشر الترابط كمفتاح:

  • السطر 59، يمكننا كتابة:

config[thread_name][‘cookies’]=cookies
  • في السطر 52، يمكننا بعد ذلك كتابة:

cookies=config[thread_name][‘cookies’]

هنا، سنستخدم تقنية مختلفة: سيكون لكل مؤشر ترابط (=عميل) طبقة [dao] خاصة به. بهذه الطريقة، لن يكون السطر 59 مشكلة بعد الآن لأن ملفات تعريف الارتباط المستخدمة هي تلك الخاصة بعميل واحد.

للقيام بذلك، سننشئ فئة جديدة [ImpôtsDaoWithHttpSessionFactory].

25.3.1.2. وظيفة فك تشفير جلسة Flask

يتم تعريف وظيفة [decode_flask_session] في البرنامج النصي [myutils]:

Image

لقد درسنا بالفعل البرنامج النصي |myutils|. هذا البرنامج النصي هو وحدة نمطية على نطاق الآلة يمكن للبرامج النصية المختلفة في هذه الدورة استيرادها باستخدام العبارة:

import myutils

فيه، نُعرّف الدالة [decode_flask_session] على النحو التالي:

def decode_flask_session(cookie: str) -> str:
    #  source : https://www.kirsle.net/wizards/flask-session.cgi
    compressed = False
    payload = cookie

    if payload.startswith('.'):
        compressed = True
        payload = payload[1:]

    data = payload.split(".")[0]

    data = base64_decode(data)
    if compressed:
        data = zlib.decompress(data)

    return data.decode("utf-8")
  • السطر 2: عنوان URL الذي وجدت فيه هذه الوظيفة؛
  • السطر 1: المعلمة [cookie] هي السلسلة المرتبطة بمفتاح [session] في قاموس ملفات تعريف الارتباط التي يتلقاها عميل الويب؛
  • الأسطر 3–16: لن أعلق على هذا الكود، لأنني لا أفهمه تمامًا؛

نضيف استيرادًا جديدًا إلى ملف [__init__.py]:


from .myutils import set_syspath, json_response, decode_flask_session

يتم تثبيت الإصدار الجديد من [myutils] ضمن الوحدات النمطية على مستوى الجهاز باستخدام الأمر [pip install .] في محطة PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
  Attempting uninstall: myutils
    Found existing installation: myutils 0.1
    Uninstalling myutils-0.1:
      Successfully uninstalled myutils-0.1
    Running setup.py install for myutils ... done
Successfully installed myutils-0.1
  • السطر 1: يجب أن تكون في مجلد [packages] لإدخال هذا الأمر؛

25.3.1.3. فئة [ImpôtsDaoWithHttpSessionFactory]

فئة [ImpôtsDaoWithHttpSessionFactory] هي كما يلي:

from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession


class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        #  the parameter
        self.__config = config

    def new_instance(self):
        #  render an instance of the [dao] layer
        return ImpôtsDaoWithHttpSession(self.__config)
  • تسمح لنا فئة [ImpôtsDaoWithHttpSessionFactory] بإنشاء تطبيق جديد لطبقة [dao] باستخدام طريقة [new_instance] في الأسطر 10–12؛

25.3.2. التكوين

يتم تعديل البرنامج النصي [config_layers]، الذي يقوم بتكوين طبقات عميل الويب، على النحو التالي:

def configure(config: dict) -> dict:
    #  instantiation of applicatuon layers

    #  dao layer
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    #  make the layer configuration
    return {
        "dao_factory": dao_factory
    }
  • السطران 5-6: بدلاً من إنشاء مثيل لطبقة [DAO] واحدة كما كان يحدث سابقًا، نقوم بإنشاء مثيل لـ "مصنع" لهذه الطبقة (المصنع = مصنع إنتاج الكائنات، وفي هذه الحالة طبقة [DAO]
  • السطور 9-11: نُرجع تكوين الطبقة؛

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

تغير البرنامج النصي [الرئيسي] على النحو التالي مقارنة بالإصدار السابق:

#  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(thread_dao, logger, taxpayers: list):
    


#  list of client threads
threads = []
logger = None
#  code
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        #  each thread must have its own [dao] layer to properly manage its session cookie
        thread_dao = dao_factory.new_instance()
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(thread_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
    
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()
  • السطران 29-30: كل مؤشر ترابط له طبقة [dao] خاصة به؛

25.3.4. تنفيذ العميل

يتم تشغيل خادم الويب، ويتم تشغيل نظام إدارة قواعد البيانات (DBMS)، ويتم تشغيل خادم البريد [hMailServer]. ثم نقوم بتشغيل البرنامج النصي [main] لعميل الويب. وتكون سجلات التنفيذ في [data/logs/logs.txt] كما يلي:


2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"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-25 10:21:46.725910, Thread-4 : 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-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"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-25 10:21:47.514939, Thread-3 : 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-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"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-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, 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-25 10:21:47.528341, Thread-5 : 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-25 10:21:47.528341, 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-25 10:21:47.528842, 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-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, 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-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"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-25 10:21:47.537161, Thread-6 : {"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-25 10:21:47.538156, Thread-3 : 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-25 10:21:47.538557, Thread-6 : 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-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, 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-25 10:21:47.546198, 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-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, 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-25 10:21:47.739643, 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-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, 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-25 10:21:48.558715, 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-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, 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-25 10:21:48.753318, 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-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
  • يوجد إجمالي 6 سلاسل، مما يعني 6 عملاء (الأسطر 1، 3، 4، 6، 8، 9) يستعلمون في وقت واحد عن خادم حساب الضرائب؛
  • سنتابع الخيط [Thread-4الذي يتعامل مع 3 دافعي ضرائب (السطر 6). سيقوم بإرسال ثلاثة طلبات متتالية إلى خادم حساب الضرائب؛
  • السطر 10: الطلب الأول لـ [Thread-4]؛
  • السطر 13: تلقى [Thread-4] الرد على طلبه الأول. وفيه، يجد ملف تعريف ارتباط جلسة يحتوي على الرقم [fa3c83b82761c83e13217967] الذي خصصه له الخادم؛
  • السطر 14: الضريبة الخاصة بالمكلف الأول؛
  • السطر 16: [Thread-4] يقدم طلبًا للمكلف الثاني؛
  • السطر 43: [Thread-4] يتلقى مبلغ الضريبة للمكلف الثاني؛
  • السطر 45: [Thread-4] يقدم طلبًا للمكلف الثالث؛
  • السطر 49: [Thread-4] يتلقى مبلغ الضريبة للمكلف الثالث؛
  • السطر 51: [Thread-4] قد أنهى عمله؛

الآن، دعونا نلقي نظرة على كيفية معالجة الطلبات الثلاثة الواردة من [Thread-4] على جانب الخادم. يمكننا تتبعها في سجلات الخادم باستخدام معرف العميل الخاص بها [fa3c83b82761c83e13217967].

سجلات جانب الخادم [data/logs/logs.txt] هي كما يلي:


2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [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-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [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-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [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-25 10:21:47.510506, Thread-4 : [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}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [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-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [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-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=2&salaire=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [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-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [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-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [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-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [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-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [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}}}
  • يظهر العميل [fa3c83b82761c83e13217967] لأول مرة في السطر 14: لحساب الضريبة، كان على الخادم استرداد البيانات من قاعدة بيانات مصلحة الضرائب؛
  • ثم نرى العميل [fa3c83b82761c83e13217967] مرة أخرى في السطر 36. هذه المرة، يسترد الخادم بيانات سلطة الضرائب من الجلسة، مما يوفر عليه وصولاً قد يكون مكلفاً إلى طبقة [DAO]؛
  • نلتقي بالعميل [fa3c83b82761c83e13217967] للمرة الثالثة في السطر 42، حيث يستخدم الخادم مرة أخرى جلسة عمل العميل؛

يوضح هذا المثال بوضوح قيمة الجلسة بالنسبة للعميل: فهي تخزن البيانات المشتركة بين جميع طلبات ذلك العميل، والتي يكون استرجاعها مكلفًا.

على جانب العميل، النتائج الموجودة في الملف [data/output/results.json] هي نفسها الموجودة في الإصدارات السابقة.

25.4. اختبار طبقة [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
    #  retrieve the [dao] layer factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()

    #  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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
 
OK
 
Process finished with exit code 0