Skip to content

31. عملاء الويب لخدمات JSON و XML للإصدار 12

سنكتب ثلاثة تطبيقات عميل وحدة التحكم لخدمات JSON و XML لخادم الويب الذي كتبناه للتو. سنعيد استخدام بنية العميل/الخادم من الإصدار 11:

Image

سنكتب ثلاثة نصوص برمجية للوحدة:

  • ستستخدم البرامج النصية [main] و [main3] طبقة [business] الخاصة بالخادم؛
  • سيستخدم البرنامج النصي [main2] طبقة [business] الخاصة بالعميل؛

31.1. هيكل دليل نصوص العميل

يتم إنشاء المجلد [http-clients/07] مبدئيًا عن طريق نسخ المجلد [http-clients/06]. ثم يتم تعديله.

Image

  • في [1]: البيانات التي يستخدمها العميل أو ينشئها؛
  • في [2]، نصوص تكوين العميل ونصوص وحدة التحكم؛
  • في [3]، طبقة [dao] الخاصة بالعميل؛
  • في [4]، مجلد الاختبار لطبقة [dao] الخاصة بالعميل؛

31.2. طبقة [dao] للعملاء

Image

Image

31.2.1. الواجهة

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

from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    #  tax calculation per unit
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    #  batch tax calculation
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    #  session initialization
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    #  end of session
    @abstractmethod
    def end_session(self):
        pass

    #  authentication
    @abstractmethod
    def authenticate_user(self, user: str, password: str):
        pass

    #  list of simulations
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    #  delete a simulation
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    #  obtain data for tax calculations
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

تتوافق كل طريقة من طرق الواجهة مع عنوان URL لخدمة على خادم حساب الضرائب.

  • السطر 7: تمتد الواجهة من فئة [AbstractDao]، التي تدير الوصول إلى نظام الملفات؛

يتم تعريف التعيين بين الطرق وعناوين URL للخدمات في ملف التكوين [config]:


        # le serveur de calcul de l'impôt
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }

31.2.2. التنفيذ

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

#  imports
import json

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    #  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"]
        #  services
        self.__config_services = config["server"]['url_services']
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None
        #  cookies
        self.__cookies = None
        #  session type (json, xml)
        self.__session_type = None

        # étape request / response
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        #  [method]: HTTP GET or POST method
        #  [url_service] : URL of service
        #  [data]: POST parameters in x-www-form-urlencoded
        #  [json]: POST parameters in json
        #  [cookies]: cookies to include in the request

        #  you must have a XML or JSON session, otherwise you won't be able to handle the response
        if self.__session_type not in ['json', 'xml']:
            raise ImpôtsError(73, "il n'y a pas de session valide en cours")

        #  connection
        if method == "GET":
            #  GET
            response = requests.get(url_service, cookies=self.__cookies)
        else:
            #  POST
            response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)

        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")

        #  result
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  #  xml
            résultat = xmltodict.parse(response.text[39:])['root']

        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies

        #  status code
        status_code = response.status_code

        #  if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        #  we return the result
        return résultat['réponse']

    def init_session(self, session_type: str):
        #  note the session type
        self.__session_type = session_type

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        #  request execution
        self.get_response("GET", url_service)

  • الأسطر 16–34: منشئ الفئة؛
  • السطر 19: يتم تهيئة الفئة الأم؛
  • الأسطر 21–28: يتم تخزين بيانات تكوين معينة؛
  • الأسطر 29–34: يتم إنشاء ثلاث خصائص تُستخدم في أساليب الفئة؛
  • الأسطر 36–82: تقوم طريقة [get_response] بفصل العناصر المشتركة بين جميع الطرق في طبقة [dao]: إرسال طلب HTTP واسترداد استجابة HTTP من الخادم؛
  • الأسطر 38–42: تعريف المعلمات الخمس لطريقة [get_response]؛
  • السطر 42: لاحظ أنه نظرًا لأن الخادم يحافظ على الجلسة، يحتاج العميل إلى قراءة/إرسال ملفات تعريف الارتباط؛
  • الأسطر 44–46: نتحقق من وجود جلسة عمل صالحة بالفعل؛
  • السطر 51: حالة GET. يتم إرسال ملفات تعريف الارتباط المستلمة مرة أخرى؛
  • السطر 54: حالة POST. يمكن أن تحتوي هذه الحالة على نوعين من المعلمات:
    • النوع [x-www-form-urlencoded]. هذا هو الحال بالنسبة لعناوين URL [/calculate-tax] و [/authenticate-user]. ثم نستخدم المعلمة [data_value] التي استلمتها الطريقة؛
    • النوع [json]. هذا هو الحال بالنسبة لعنوان URL [/calculate-taxes]. ثم نستخدم المعلمة [json_value] التي استلمتها الطريقة؛

هنا أيضًا، يتم إرجاع ملف تعريف ارتباط الجلسة.

  • الأسطر 56–62: إذا كنا في وضع [debug]، يتم تسجيل استجابة الخادم. هذا السجل مهم لأنه يسمح لنا بمعرفة ما أعاده الخادم بالضبط؛
  • الأسطر 64–68: اعتمادًا على ما إذا كنا في وضع JSON أو XML، يتم تحويل استجابة الخادم النصية إلى قاموس. لنأخذ مثال عنوان URL [/init-session]:

استجابة JSON هي كما يلي:


2020-08-03 11:45:21.218116, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}

الاستجابة XML هي كما يلي:


2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>

يضمن الكود في الأسطر 64–68 أنه، في كلتا الحالتين، تحتوي [result] على قاموس بمفاتيح [action, status, response]؛

  • الأسطر 70–72: إذا كانت الاستجابة تحتوي على ملفات تعريف الارتباط، يتم استردادها. ويجب إرسالها مرة أخرى مع الطلب التالي؛
  • الأسطر 74–79: إذا لم تكن حالة HTTP للاستجابة هي 200، يتم إثارة استثناء مع رسالة الخطأ الموجودة في result[‘response’]. يمكن أن يكون هذا خطأً واحدًا أو قائمة من الأخطاء؛
  • الأسطر 81–82: إرجاع استجابة الخادم إلى الكود المستدعي؛

[init_session]

  • السطر 84: تُستخدم طريقة [init_session] لتعيين نوع الجلسة (JSON أو XML) التي يرغب العميل في بدءها مع الخادم؛
  • السطر 86: يتم تخزين نوع الجلسة المطلوب داخل الفئة. في الواقع، تتطلب جميع الطرق هذه المعلومات لفك تشفير استجابة الخادم بشكل صحيح؛
  • الأسطر 88-90: باستخدام تكوين التطبيق، يتم تحديد عنوان URL للخدمة المراد الاستعلام عنها؛
  • السطر 93: يتم الاستعلام عن عنوان URL للخدمة. لا يتم استرداد نتيجة طريقة [get_response]:
    • إذا أطلقت استثناءً، فهذا يعني أن العملية قد فشلت. لا يتم التعامل مع الاستثناء هنا وسيتم تمريره مباشرةً إلى الكود المستدعي، والذي سيقوم بعد ذلك بإنهاء عمل العميل مع رسالة خطأ؛
    • إذا لم تحدث استثناء، فإن تهيئة الجلسة قد نجحت؛

[authenticate_user]

    def authenticate_user(self, user: str, password: str):
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
        post_params = {
            "user": user,
            "password": password
        }

        #  request execution
        self.get_response("POST", url_service, post_params)
  • تُستخدم طريقة [authenticate_user] للمصادقة مع الخادم. للقيام بذلك، تستقبل بيانات اعتماد تسجيل الدخول [user, password] في السطر 1؛
  • الأسطر 2–4: نحدد عنوان URL للخدمة المراد الاستعلام عنها؛
  • الأسطر 5-8: معلمات POST، نظرًا لأن عنوان URL [/authenticate-user] يتوقع طلب POST مع المعلمات [user, password]؛
  • السطر 11: يتم تنفيذ الطلب. مرة أخرى، لا نسترد استجابة الخادم. إن الاستثناء الذي ترمي به [get_response] هو الذي يشير إلى ما إذا كانت العملية ناجحة أم لا؛

[calculate_tax]

    def calculate_tax(self, taxpayer: TaxPayer):
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
        #  POST parameters
        post_params = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        #  request execution
        response = self.get_response("POST", url_service, post_params)
        #  update the TaxPayer with the response
        taxpayer.fromdict(response)
  • تحسب الطريقة [calculate_tax] الضريبة لمكلف [taxpayer] تم تمريره كمعلمة. يتم تعديل هذه المعلمة بواسطة الطريقة (السطر 15) وبالتالي تشكل نتيجة الطريقة؛
  • الأسطر 2–4: نحدد عنوان URL للخدمة المراد الاستعلام عنها؛
  • الأسطر 6-10: المعلمات لطلب POST المراد إرساله. يتوقع عنوان URL للخدمة [/calculate-tax] طلب POST مع المعلمات [married, children, salary]؛
  • السطور 12-13: يتم تنفيذ الطلب واسترداد استجابة الخادم. يعرض عنوان URL للخدمة [/calculate-tax] قاموسًا بمفاتيح الضريبة [tax, discount, surcharge, reduction, rate]؛
  • السطر 15: يتم استخدام القاموس الذي تم الحصول عليه [response] لتحديث دافع الضرائب [taxpayer]؛

[calculate_tax_in_bulk_mode]

    #  bulk tax calculation
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        #  we let the exceptions rise

        #  transform taxpayers into a list of dictionaries
        #  we keep only the properties [married, children, salary]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        #  request execution
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        #  when there's only one taxpayer and you're in an xml session, [list_dict_taxpayers2] isn't a list
        #  in this case we make a list
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        #  the initial taxpayer list is updated with the results received
        for i in range(len(taxpayers)):
            #  taxpayers[i] update
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        #  here the [taxpayers] parameter has been updated with the server results
  • السطر 2: تتلقى الطريقة قائمة بالدافعين من النوع TaxPayer؛
  • الأسطر 7–13: يتم تحويل قائمة عناصر [TaxPayer] هذه إلى قائمة من القواميس [spouse, children, salary]؛
  • الأسطر 15-17: يتم تعيين عنوان URL للخدمة؛
  • السطور 19-20: يتم تنفيذ طلب POST، مع نص JSON يتكون من قائمة القواميس التي تم إنشاؤها في السطر 7. يتم استرداد استجابة الخادم؛
  • السطور 23–24: كشفت الاختبارات عن مشكلة عندما تكون الجلسة من النوع XML:
    • إذا كانت القائمة الأولية لدافعي الضرائب تحتوي على N عنصرًا (N>1)، فإن النتيجة هي قائمة من N قواميس من النوع [OrderedDict]؛
    • إذا كانت القائمة الأولية تحتوي على عنصر واحد فقط، فإن النتيجة ليست قائمة بل عنصر واحد من النوع [OrderedDict]؛
  • السطران 23-24: إذا كان هذا هو الحال (عنصر واحد)، فإننا نحول النتيجة إلى قائمة من عنصر واحد؛
  • الأسطر 25-28: تحتوي قائمة القواميس المستلمة هذه على مبلغ الضريبة لكل دافع ضرائب في القائمة الأولية. ثم نقوم بتحديث كل منها بالنتائج المستلمة؛

[get_simulations]

1
2
3
4
5
6
7
    def get_simulations(self) -> list:
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        #  request execution
        return self.get_response("GET", url_service)
  • السطر 1: تطلب الطريقة قائمة المحاكاة التي تم إجراؤها في الجلسة الحالية؛
  • السطر 2: تعيد الطريقة استجابة الخادم؛

[delete_simulation]

1
2
3
4
5
6
7
    def delete_simulation(self, id: int) -> list:
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        #  request execution
        return self.get_response("GET", url_service)
  • السطر 1: تقوم الطريقة بحذف المحاكاة التي تم تمرير معرفها؛
  • السطر 7: تعيد استجابة الخادم، وهي قائمة المحاكاة المتبقية بعد الحذف المطلوب؛

[get-admindata]

    def get_admindata(self) -> AdminData:
        #  we let the exceptions rise

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        #  request execution
        résultat = self.get_response("GET", url_service)

        #  result is a dictionary of str values if xml session
        if self.__session_type == 'xml':
            #  new dictionary
            résultat2 = {}
            #  we take everything digital
            for key, value in résultat.items():
                #  some dictionary elements are lists
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    #  others simple elements
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        #  result type AdminData
        return AdminData().fromdict(résultat2)
  • السطر 1: تطلب الطريقة الثوابت الضريبية من الخادم لحساب الضريبة؛
  • السطر 29: تُرجع نوع [AdminData]؛
  • السطر 9: نسترد استجابة الخادم في شكل قاموس. تُظهر الاختبارات وجود مشكلة عندما تكون الجلسة جلسة XML: فبدلاً من أن تكون القيم أرقامًا، تكون القيم في القاموس سلاسل نصية. كنا قد أبلغنا عن هذه المشكلة أثناء دراسة وحدة [xmltodict] ووجدنا أن هذا سلوك طبيعي. لا تحتوي [xmltodict] على معلومات عن النوع في دفق XML الذي يتم تزويدها به. ومع ذلك، في هذه الحالة المحددة، يجب تحويل جميع القيم في القاموس المستلم إلى قيم رقمية. يحتوي هذا القاموس على ثلاث قوائم [limites، coeffr، coeffn] وسلسلة من الخصائص الرقمية؛
  • الأسطر 13–25: إنشاء قاموس [result2] بقيم رقمية من قاموس [result] الذي يحتوي على قيم من نوع سلسلة؛
  • السطر 29: يتم استخدام القاموس [result2] لتهيئة نوع [AdminData]؛

31.2.3. مصنع طبقة [dao]

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

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)

31.3. تكوين العميل

Image

يتم تكوين العملاء باستخدام ملفات [config] و [config_layers]. ملف [config] هو كما يلي:

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ôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        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",
            "user": {
                "login": "admin",
                "password": "admin"
            },
            "url_services": {
                "calculate-tax": "/calculer-impot",
                "get-admindata": "/get-admindata",
                "calculate-tax-in-bulk-mode": "/calculer-impots",
                "init-session": "/init-session",
                "end-session": "/fin-session",
                "authenticate-user": "/authentifier-utilisateur",
                "get-simulations": "/lister-simulations",
                "delete-simulation": "/supprimer-simulation"
            }
        },
        #  debug mode
        "debug": True
    }
    )

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

    #  we return the configuration
    return config

ملف [config_layers] كما يلي:

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

    #  layer [profession]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

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

    #  make the layer configuration
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • لن يتمكن العملاء من الوصول المباشر إلى طبقة [dao]. وللوصول إليها، يجب عليهم المرور عبر مصنع طبقة [dao]؛

31.4. العميل [main]

يتيح لك عميل [main] اختبار عناوين URL [/init-session، /authenticate-user، /calculate-taxes، /end-session]:

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({"session_type": session_type})

#  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(config: dict, taxpayers: list):
    #  retrieve the [dao] layer factory
    dao_factory = config['layers']['dao_factory']
    #  create a [dao] layer instance
    dao = dao_factory.new_instance()
    #  session type
    session_type = config['session_type']
    #  number of taxpayers
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  end of session
    dao.end_session()
    #  log
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  start log
    logger.write("début du calcul de l'impôt des contribuables\n")
    #  retrieve the [dao] layer factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  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)]
        #  i is incremented for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(config, 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)
    #  end
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        #  end log
        logger.write("fin du calcul de l'impôt des contribuables\n")
        #  closing the logger
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • الأسطر 4-11: يتوقع العميل معلمة تحدد نوع الجلسة، JSON أو XML، لاستخدامها مع الخادم؛
  • الأسطر 13-15: تم تكوين العميل؛
  • الأسطر 48–104: هذا الكود مألوف. وقد استُخدم مرات عديدة. وهو يوزع دافعي الضرائب الذين نريد حساب الضريبة عليهم على عدة خيوط؛
  • السطر 26: طريقة [thread_function] هي الطريقة التي تنفذها كل خيط لحساب الضريبة للمكلفين المخصصين له؛
  • الأسطر 27–30: لكل مؤشر ترابط طبقة [dao] خاصة به؛
  • يتم حساب الضريبة في أربع خطوات:
    • السطور 37-38: تهيئة جلسة (JSON أو XML) مع الخادم؛
    • السطران 39-40: المصادقة مع الخادم؛
    • السطور 41-42: حساب الضريبة؛
    • الأسطر 43-44: إغلاق الجلسة مع الخادم؛

عند تنفيذ هذا الرمز في الوضع [json]، يتم إنشاء السجلات التالية:


2020-08-03 14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables

يوضح ما سبق مسار تنفيذ الخيط [Thread-2].

إذا قمنا بتشغيل [main] في وضع XML، فستكون السجلات كما يلي:


2020-08-03 14:32:48.495316, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables

فيما يلي مسار مؤشر ترابط [Thread-2].

31.5. العميل [main2]

Image

يتيح لك العميل [main2] اختبار عناوين URL [/init-session، /authenticate-user، /get-admindata، /end-session]:

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({"session_type": session_type})

#  dependencies
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  start log
    logger.write("début du calcul de l'impôt des contribuables\n")
    #  retrieve the [dao] layer factory
    dao_factory = config['layers']['dao_factory']
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  we get the taxpayers back
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  session type
    session_type = config['session_type']
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  we retrieve data from the tax authorities
    admindata = dao.get_admindata()
    #  end of session
    dao.end_session()
    #  calculation of taxpayers' taxes by the [business] layer
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    #  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:
        #  end log
        logger.write("fin du calcul de l'impôt des contribuables\n")
        #  closing the logger
        logger.close()
    #  we're done
    print("Travail terminé...")
  • الأسطر 1-11: استرداد المعلمة [json, xml] التي تحدد نوع الجلسة المراد إنشاؤها مع الخادم؛
  • الأسطر 13-15: نقوم بتكوين العميل؛
  • الأسطر 30-33: نقوم بإنشاء طبقة [dao]؛
  • الأسطر 34-35: باستخدامها، نسترد قائمة دافعي الضرائب الذين يجب حساب الضريبة عليهم؛
  • ثم نمر بالخطوات الأربع للحوار مع الخادم؛
    • السطور 41–42: يتم بدء جلسة مع الخادم؛
    • السطور 43-44: نقوم بالمصادقة مع الخادم؛
    • السطور 45-46: نطلب ثوابت الضريبة من الخادم لحساب الضريبة؛
    • السطران 47-48: يتم إغلاق الجلسة مع الخادم؛
  • الأسطر 49–52: باستخدام هذه الثوابت، يمكننا حساب ضريبة دافعي الضرائب باستخدام الطبقة [التجارية] المحلية على العميل؛
  • السطران 53 و54: يتم حفظ النتائج؛

بالنسبة لجلسة XML، تكون النتائج كما يلي:


2020-08-03 14:44:43.194294, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables

31.6. العميل [main3]

يتيح لك العميل [main3] اختبار عناوين URL [/init-session، /calculate-taxes، /get-simulations، /delete-simulation، /end-session]:

Image

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({"session_type": session_type})

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

logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  start log
    logger.write("début du calcul de l'impôt des contribuables\n")
    #  retrieve the [dao] layer factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  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']}")
    #  tax calculation
    #  number of taxpayers
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  the list of simulations is requested
    simulations = dao.get_simulations()
    #  we remove one out of two
    for i in range(len(simulations)):
        if i % 2 == 0:
            #  we delete the simulation
            dao.delete_simulation(simulations[i]['id'])
    #  end of session
    dao.end_session()
    #  consult the logs to see the various results (debug mode=True)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        #  end log
        logger.write("fin du calcul de l'impôt des contribuables\n")
        #  closing the logger
        logger.close()
    #  we're done
    print("Travail terminé...")
  • الأسطر 1-11: استرداد نوع الجلسة من معلمات البرنامج النصي؛
  • الأسطر 13-15: نقوم بتكوين التطبيق؛
  • الأسطر 25-50: كود تم شرحه بالفعل في مرحلة ما؛
  • السطور 51-52: نطلب قائمة المحاكاة التي تم إجراؤها في الجلسة الحالية؛
  • الأسطر 53-57: حذف كل محاكاة أخرى؛
  • السطور 58–59: يتم إنهاء الجلسة؛

خلال جلسة jSON، تكون السجلات كما يلي:


2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
  • السطر 6: لدينا 11 محاكاة؛
  • السطر 12: بعد عمليات الحذف المختلفة، لم يتبق سوى 5؛

31.7. فئة الاختبار [Test2HttpClientDaoWithSession]

Image

تختبر فئة [Test2HttpClientDaoWithSession] طبقة [dao] للعملاء على النحو التالي:

import unittest

from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer

class Test2HttpClientDaoWithSession(unittest.TestCase):

    def test_init_session_json(self) -> None:
        print('test_init_session_json')
        erreur = False
        try:
            dao.init_session('json')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be no error
        self.assertFalse(erreur)

    def test_init_session_xml(self) -> None:
        print('test_init_session_xml')
        erreur = False
        try:
            dao.init_session('xml')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  here must be no errors
        self.assertFalse(erreur)

    def test_init_session_xxx(self) -> None:
        print('test_init_session_xxx')
        erreur = False
        try:
            dao.init_session('xxx')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        #  init session
        dao.init_session('json')
        #  test
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be no errors
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        #  init session
        dao.init_session('json')
        #  test
        erreur = False
        try:
            dao.authenticate_user('x', 'y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        #  init session
        dao.init_session('json')
        #  authentication
        dao.authenticate_user('admin', 'admin')
        #  tax calculation
        #  { '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)
        #  get_simulations
        simulations = dao.get_simulations()
        #  checks
        #  there must be 1 simulation
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        #  verification of calculated tax
        self.assertAlmostEqual(simulation['impôt'], 2815, delta=1)
        self.assertEqual(simulation['décôte'], 0)
        self.assertEqual(simulation['réduction'], 0)
        self.assertAlmostEqual(simulation['taux'], 0.14, delta=0.01)
        self.assertEqual(simulation['surcôte'], 0)

    def test_delete_simulation(self) -> None:
        print('test_delete_simulation')
        #  init session
        dao.init_session('json')
        #  authentication
        dao.authenticate_user('admin', 'admin')
        #  tax calculation
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  get_simulations
        simulations = dao.get_simulations()
        #  delete_simulation
        dao.delete_simulation(simulations[0]['id'])
        #  get_simulations
        simulations = dao.get_simulations()
        #  verification - there should be no more simulations
        self.assertEqual(0, len(simulations))
        #  delete a non-existent simulation
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

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

    #  layer [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    #  test methods are executed
    print("tests en cours...")
    unittest.main()
  • ترسل طبقة [dao] طلبًا إلى الخادم، وتستقبل الرد، وتقوم بتنسيقه لإعادته إلى الكود المستدعي. عندما يرسل الخادم ردًا برمز حالة غير 200، تثير طبقة [dao] استثناءً. لذلك، يتضمن عدد من الاختبارات التحقق مما إذا كان قد حدث استثناء أم لا؛
  • الأسطر 9–18: نقوم بتهيئة جلسة JSON. لا ينبغي أن تكون هناك أخطاء؛
  • الأسطر 20-29: نقوم بتهيئة جلسة XML. لا ينبغي أن يكون هناك أي خطأ؛
  • الأسطر 31-40: نقوم بتهيئة جلسة بنوع غير صحيح. يجب أن يحدث خطأ؛
  • الأسطر 42-54: نقوم بالمصادقة باستخدام بيانات الاعتماد الصحيحة. لا ينبغي أن يكون هناك أي خطأ؛
  • الأسطر 56-68: المصادقة باستخدام بيانات اعتماد غير صحيحة. يجب أن يحدث خطأ؛
  • الأسطر 70-92: نحسب الضريبة ثم نطلب قائمة المحاكاة. يجب أن نحصل على واحدة. بالإضافة إلى ذلك، نتحقق من أن هذه المحاكاة تحتوي على الضريبة المطلوبة؛
  • الأسطر 94–119: يتم إنشاء محاكاة ثم حذفها. ثم تتم محاولة حذف محاكاة على الرغم من عدم وجود محاكاة متبقية. يجب أن يحدث خطأ؛
  • الأسطر 121–137: يتم تشغيل الاختبار كنص برمجي قياسي لوحدة التحكم؛
  • الأسطر 122–124: نقوم بتكوين التطبيق؛
  • الأسطر 126–129: نقوم بتكوين المسجل. سيسمح لنا ذلك بتتبع السجلات؛
  • الأسطر 131–133: نقوم بإنشاء مثيل لطبقة [DAO] التي سيتم اختبارها؛
  • الأسطر 135–137: تشغيل الاختبارات؛

إخراج وحدة التحكم كما يلي:


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/07/tests/Test2HttpClientDaoWithSession.py
tests en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s
 
OK
 
Process finished with exit code 0