Skip to content

31. کلاینت‌های وب برای سرویس‌های jSON و XML در نسخه ۱۲

ما قصد داریم سه برنامهٔ کلاینت کنسول برای سرویس‌های jSON و XML وب‌سروری که همین‌الان نوشته‌ایم، بنویسیم. ما از معماری کلاینت/سرور نسخهٔ 11 استفاده مجدد می‌کنیم:

Image

ما سه اسکریپت کنسول خواهیم نوشت:

  • اسکریپت‌های [main] و [main3] از لایه [métier] سرور استفاده خواهند کرد؛
  • اسکریپت [main2] از لایه کلاینت [métier] استفاده خواهد کرد؛

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

    #محاسبه مالیات به ازای هر واحد
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    #محاسبه مالیات به‌صورت دسته‌ای
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    # ابتدای جلسه
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    #پایان جلسه
    @abstractmethod
    def end_session(self):
        pass

    #احراز هویت
    @abstractmethod
    def authenticate_user(self, user: str, password: str):
        pass

    # فهرست شبیه‌سازی‌ها
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    # حذف یک شبیه‌سازی
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    #بازیابی داده‌ها برای محاسبه مالیات
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

هر متد این رابط با یک کلاس سرویس URL در سرور محاسبه مالیات مطابقت دارد.

  • خط ۷: این رابط کلاس [AbstractDao] را که مدیریت دسترسی به سیستم فایل را بر عهده دارد، گسترش می‌دهد؛

نقشه بین متدها و سرویس URL در فایل پیکربندی [config] تعریف شده است:


        # سرور محاسبه مالیات
        "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] پیاده‌سازی شده است:


# واردات‌ها
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):

    # تولیدکننده
    def __init__(self, config: dict):
        # ابتدایی‌سازی والد
        AbstractImpôtsDao.__init__(self, config)
        #ذخیرهٔ عناصر پیکربندی
        # پیکربندی عمومی
        self.__config = config
        # سرور
        self.__config_server = config["server"]
        # خدمات
        self.__config_services = config["server"]['url_services']
        # حالت اشکال‌زدایی
        self.__debug = config["debug"]
        # لاگ‌گیر
        self.__logger = None
        # کوکی‌ها
        self.__cookies = None
        # نوع جلسه (json, xml)
        self.__session_type = None

        # مرحله درخواست/پاسخ
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: روش HTTP, GET یا POST
        #[url_service]: سرویس URL
        # [data]: پارامترها برای POST در قالب x-www-form-urlencoded
        # [json]: پارامترهای POST در قالب JSON
        #[cookies]: کوکی‌هایی که باید در درخواست گنجانده شوند

        #شما باید یک جلسه (XML یا JSON) داشته باشید؛ در غیر این صورت قادر به پردازش پاسخ نخواهید بود
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        #ورود
        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)

        # حالت اشکال‌زدایی؟
        if self.__debug:
            # لاگ‌گیر
            if not self.__logger:
                self.__logger = self.__config['logger']
            # ثبت
            self.__logger.write(f"{response.text}\n")

        # نتیجه
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  # XML
            résultat = xmltodict.parse(response.text[39:])['root']

        # در صورت وجود، کوکی‌ها را از پاسخ بازیابی می‌کند
        if response.cookies:
            self.__cookies = response.cookies

        # کد وضعیت
        status_code = response.status_code

        # اگر کد وضعیت ۲۰۰ نباشد OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # نتیجه را بازمی‌گرداند
        return résultat['réponse']

    def init_session(self, session_type: str):
        # نوع جلسه را ثبت می‌کند
        self.__session_type = session_type

        # URL سرویس
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # اجرای درخواست
        self.get_response("GET", url_service)

  • خطوط 16–34: سازنده کلاس؛
  • خط ۱۹: کلاس والد مقداردهی اولیه می‌شود؛
  • خطوط ۲۱–۲۸: برخی داده‌های پیکربندی ذخیره می‌شوند؛
  • خطوط ۲۹–۳۴: سه ویژگی که در متدهای کلاس استفاده می‌شوند، ایجاد می‌شوند؛
  • خطوط ۳۶–۸۲: متد [get_response] عناصر مشترک را که بین تمام متدها در لایه [dao] مشترک هستند، استخراج می‌کند: ارسال یک درخواست HTTP و دریافت پاسخ HTTP از سرور؛
  • خطوط ۳۸–۴۲: تعریف پنج پارامتر روش [get_response
  • خط ۴۲: توجه داشته باشید که از آنجا که سرور یک جلسه را حفظ می‌کند، کلاینت باید کوکی‌ها را بخواند و بازگرداند؛
  • خطوط ۴۴–۴۶: بررسی می‌کنیم که واقعاً یک جلسه فعال معتبر وجود دارد؛
  • خط ۵۱: مورد GET. کوکی‌های دریافت‌شده بازگردانده می‌شوند؛
  • خط ۵۴: مورد POST. این می‌تواند دو نوع پارامتر داشته باشد:
    • نوع [x-www-form-urlencoded]. این برای URL، [/calculer-impot] و [/authentifier-utilisateur] اعمال می‌شود. در این حالت، از پارامتر [data_value] دریافتی توسط متد استفاده می‌شود؛
    • نوع [json]. این برای URL و [/calculer-impots] اعمال می‌شود. سپس از پارامتر [json_value] که توسط متد دریافت شده است استفاده می‌شود؛

در اینجا نیز کوکی جلسه بازگردانده می‌شود.

  • خطوط ۵۶–۶۲: اگر در حالت [debug] باشید، پاسخ سرور ثبت می‌شود. این لاگ مهم است زیرا به ما امکان می‌دهد دقیقاً بدانیم سرور چه چیزی را بازگردانده است؛
  • خطوط ۶۴–۶۸: بسته به اینکه حالت 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>

کد در خطوط ۶۴–۶۸ تضمین می‌کند که در هر دو حالت، [résultat] یک دیکشنری با کلیدهای [action, état, réponse] را بازمی‌گرداند؛

  • خطوط ۷۰–۷۲: اگر پاسخ حاوی کوکی‌ها باشد، این کوکی‌ها بازیابی می‌شوند. لازم است در درخواست بعدی ارسال شوند؛
  • خطوط ۷۴–۷۹: اگر وضعیت پاسخ HTTP برابر با ۲۰۰ نباشد، یک استثنا با پیام خطایی که در résultat[’réponse’] آمده است، ایجاد می‌شود. این ممکن است یک خطای تکی یا فهرستی از خطاها باشد؛
  • خطوط ۸۱–۸۲: پاسخ سرور به کد فراخوانی بازگردانده می‌شود؛

[init_session]

  • خط ۸۴: متد [init_session] برای تعیین نوع جلسه (JSON یا XML) که کلاینت مایل به راه‌اندازی آن با سرور است، استفاده می‌شود؛
  • خط ۸۶: نوع جلسهٔ مورد نظر در داخل کلاس ثبت می‌شود. در واقع، همهٔ متدها برای رمزگشایی صحیح پاسخ سرور به این اطلاعات نیاز دارند؛
  • خطوط ۸۸–۹۰: با استفاده از پیکربندی برنامه، سرویس URL که باید پرس‌وجو شود، تعیین می‌شود؛
  • خط ۹۳: از سرویس URL پرس‌وجو می‌شود. نتیجه متد [get_response] بازیابی نمی‌شود:
    • اگر خطایی (exception) رخ دهد، عملیات ناموفق بوده است. این خطا در اینجا مدیریت نمی‌شود و مستقیماً به کد فراخوانی منتقل خواهد شد، که سپس کلاینت را با یک پیام خطا خاتمه می‌دهد؛
    • اگر خطا پرتاب نشود، در این صورت راه‌اندازی جلسه با موفقیت انجام شده است؛

[authenticate_user]


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

        #اجرای درخواست
        self.get_response("POST", url_service, post_params)
  • روش [authenticate_user] برای احراز هویت با سرور استفاده می‌شود. برای این کار، در خط ۱ اعتبارنامه‌های ورود [user, password] را دریافت می‌کند؛
  • خطوط ۲–۴: سرویس URL که باید پرس‌وجو شود، تعیین می‌شود؛
  • خطوط ۵–۸: پارامترها برای POST، همانند URL و [/authentifier-utilisateur] در انتظار یک POST با پارامترهای [user, password] هستند؛
  • خط ۱۱: درخواست اجرا می‌شود. مجدداً، پاسخ سرور دریافت نمی‌شود. این استثنا (exception) است که توسط [get_response] پرتاب می‌شود که نشان می‌دهد عملیات موفقیت‌آمیز بوده یا خیر؛

[calculate_tax]


    def calculate_tax(self, taxpayer: TaxPayer):
        # URL سرویس
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
        #پارامترها برای POST
        post_params = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        # اجرای درخواست
        response = self.get_response("POST", url_service, post_params)
        #به‌روزرسانی TaxPayer با پاسخ
        taxpayer.fromdict(response)
  • متد [calculate_tax] برای محاسبه مالیات برای مالیات‌دهنده [taxpayer] که به عنوان پارامتر ارسال شده است، استفاده می‌شود. این پارامتر توسط متد (خط 15) تغییر داده می‌شود و بنابراین نتیجه متد را تشکیل می‌دهد؛
  • خطوط ۲–۴: سرویس URL که باید پرس‌وجو شود، تعریف شده است؛
  • خطوط ۶–۱۰: پارامترها برای ارسال به POST. در واقع، سرویس URL منتظر یک [/calculer-impot] با پارامترهای [marié, enfants, salaire] است؛
  • خطوط ۱۲–۱۳: درخواست اجرا شده و پاسخ سرور دریافت می‌شود. سرویس URL برای [/calculer-impot] یک دیکشنری حاوی کلیدهای مالیاتی [impôt, décôte, surcôte, réduction, taux] را بازمی‌گرداند؛
  • خط ۱۵: دیکشنری حاصل [response] برای به‌روزرسانی مالیات‌دهنده [taxpayer] استفاده می‌شود؛

[calculate_tax_in_bulk_mode]


    #محاسبه مالیات در حالت دسته‌ای
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # اجازه می‌دهد استثناها گزارش شوند

        #تبدیل مودیان به فهرستی از دیکشنری‌ها
        #فقط ویژگی‌های [marié, enfants, salaire] را نگه می‌دارد
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        # URL سرویس
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        # اجرای پرس‌وجو
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        #وقتی تنها یک مؤدی وجود دارد و در یک جلسه XML هستیم، [list_dict_taxpayers2] یک لیست نیست
        # در این مورد، آن را به یک لیست تبدیل می‌کنیم
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        # ما فهرست اولیه مودیان را با نتایج دریافتی به‌روز می‌کنیم
        for i in range(len(taxpayers)):
            #به‌روزرسانی مودیان[i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        #در اینجا پارامتر [taxpayers] با نتایج دریافتی از سرور به‌روزرسانی شده است
  • خط ۲: متد یک لیست از مودیان مالیاتی از نوع TaxPayer را دریافت می‌کند؛
  • رده‌های ۷–۱۳: این فهرست از عناصر از نوع [TaxPayer] به فهرستی از دیکشنری‌ها از نوع [marié, enfants, salaire] تبدیل می‌شود؛
  • خطوط ۱۵–۱۷: سرویس URL تنظیم می‌شود؛
  • خطوط ۱۹–۲۰: یک درخواست POST اجرا می‌شود که بدنه آن (jSON) شامل فهرست فرهنگ لغت‌هایی است که در خط ۷ ایجاد شده‌اند. پاسخ از سرور دریافت می‌شود؛
  • خطوط 23–24: آزمایش‌ها نشان داده‌اند که در صورتی که جلسه از نوع XML باشد، مشکلی وجود دارد:
    • اگر فهرست اولیه مودیان دارای N عنصر (N>1) باشد، نتیجه یک فهرست از N فرهنگ لغت از نوع [OrderedDict] است؛
    • اگر لیست اولیه تنها یک عنصر داشته باشد، نتیجه یک لیست نیست، بلکه یک عنصر واحد از نوع [OrderedDict] است؛
  • خطوط ۲۳–۲۴: اگر این مورد صدق کند (۱ عنصر)، نتیجه به یک لیست تک‌عنصری تبدیل می‌شود؛
  • خطوط ۲۵–۲۸: این لیست از فرهنگ‌های دریافت‌شده، بدهی مالیاتی هر مؤدی را از لیست اولیه در خود دارد. سپس هر یک از آن‌ها را با نتایج دریافت‌شده به‌روزرسانی می‌کنیم؛

[get_simulations]


    def get_simulations(self) -> list:
        # URL سرویس
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        #اجرای پرس‌وجو
        return self.get_response("GET", url_service)
  • خط ۱: متد لیست شبیه‌سازی‌های انجام‌شده در جلسهٔ جاری را درخواست می‌کند؛
  • خط ۲: متد پاسخ سرور را بازمی‌گرداند؛

[delete_simulation]


    def delete_simulation(self, id: int) -> list:
        # آدرس URL سرویس
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        #اجرای درخواست
        return self.get_response("GET", url_service)
  • خط ۱: این متد شبیه‌سازی‌ای را که شناسه آن ارسال شده است، حذف می‌کند؛
  • خط ۷: پاسخ سرور را بازمی‌گرداند، فهرست شبیه‌سازی‌های باقی‌مانده پس از حذف درخواستی؛

[get-admindata]


    def get_admindata(self) -> AdminData:
        #اجازه می‌دهد استثناءها منتقل شوند

        # URL سرویس
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        # اجرای پرس‌وجو
        résultat = self.get_response("GET", url_service)

        # نتیجه یک دیکشنری از مقادیر رشته‌ای است اگر جلسه XML باشد
        if self.__session_type == 'xml':
            # واژه‌نامهٔ جدید
            résultat2 = {}
            # همه چیز را به عددی تبدیل می‌کند
            for key, value in résultat.items():
                # برخی از عناصر دیکشنری لیست هستند
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    # سایر موارد عناصر ساده‌اند
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        # نتیجه از نوع AdminData
        return AdminData().fromdict(résultat2)
  • خط ۱: این متد برای محاسبه مالیات، ثابت‌های مالیاتی را از سرور درخواست می‌کند؛
  • خط ۲۹: یک نوع [AdminData] را بازمی‌گرداند؛
  • خط ۹: پاسخ سرور به صورت یک دیکشنری بازیابی می‌شود. آزمایش‌ها نشان می‌دهند که زمانی جلسه یک جلسه XML است، مشکلی وجود دارد: به جای مقادیر عددی، مقادیر موجود در دیکشنری رشته‌های کاراکتری هستند. ما این مشکل را هنگام بررسی ماژول [xmltodict] گزارش کرده بودیم و متوجه شده بودیم که این رفتار طبیعی است. در استریم XML که به [xmltodict] ارائه می‌شود، هیچ اطلاعاتی از نوع وجود ندارد. با این حال، در این مورد خاص، همه مقادیر در فرهنگ لغت دریافتی باید به شکل عددی تبدیل شوند. این فرهنگ لغت شامل سه لیست [limites, coeffr, coeffn] و یک دنباله از ویژگی‌های عددی است؛
  • خطوط ۱۳–۲۵: ایجاد یک فرهنگ لغت [résultat2] با مقادیر عددی از فرهنگ لغت [résultat] که حاوی مقادیر رشته‌ای است؛
  • خط ۲۹: دیکشنری [resultat2] برای مقداردهی اولیه یک نوع [AdminData] استفاده می‌شود؛

31.2.3. کارخانه برای لایه [dao]

مشتریان ما چندرشته‌ای خواهند بود. از آنجا که لایه [dao] توسط کلاسی با وضعیت خواندن/نوشتن (= ویژگی‌های خواندن/نوشتن) پیاده‌سازی شده است، هر رشته باید لایه [dao] خود را داشته باشد، در غیر این صورت دسترسی به داده‌های مشترک بین رشته‌ها باید همگام‌سازی شود. در اینجا ما راه‌حل اول را اتخاذ می‌کنیم. ما از کلاس [ImpôtsDaoWithHttpSessionFactory] استفاده می‌کنیم که قادر به ایجاد نمونه‌هایی از لایه [dao] است:


from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession

class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        #پارامتر ذخیره می‌شود
        self.__config = config

    def new_instance(self):
        #یک نمونه از لایه بازگردانده می‌شود [dao]
        return ImpôtsDaoWithHttpSession(self.__config)

31.3. پیکربندی کلاینت

Image

کلاینت‌ها توسط فایل‌های [config] و [config_layers] پیکربندی می‌شوند. فایل [config] به شرح زیر است:


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

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

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

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

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

    # تنظیم syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    # پیکربندی برنامه با ثابت‌ها
    config.update({
        # فایل مالیات‌دهندگان
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # فایل نتایج
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # فایل خطاها
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # فایل لاگ
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # سرور محاسبه مالیات
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "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"True
    }
    )

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

    # اعمال پیکربندی
    return config

فایل [config_layers] به شرح زیر است:


def configure(config: dict) -> dict:
    # مصداق‌سازی لایه‌های کاربردی

    #لایه [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # کارخانه لایه DAO
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # رندر کردن پیکربندی لایه
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • کلاینت‌ها دسترسی مستقیم به لایه [dao] نخواهند داشت. برای دسترسی، آن‌ها باید از طریق کارخانه لایه [dao] اقدام کنند؛

31.4. کلاینت [main]

Image

کلاینت [main] به شما امکان می‌دهد کلاینت‌های URL و [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session] را آزمایش کنید:


#انتظار یک پارامتر JSON یا XML را دارد
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()

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

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

#اجرای لایه [dao] در یک نخ
# 'مالیات‌دهندگان' یک فهرست از مالیات‌دهندگان است
def thread_function(config: dict, taxpayers: list):
    # کارخانه لایه [dao] بازیابی می‌شود
    dao_factory = config['layers']['dao_factory']
    #یک نمونه از لایه [dao] ایجاد می‌شود
    dao = dao_factory.new_instance()
    # نوع جلسه
    session_type = config['session_type']
    # تعداد مالیات‌دهندگان
    nb_taxpayers = len(taxpayers)
    # لاگ
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #جلسه راه‌اندازی می‌شود
    dao.init_session(session_type)
    #احراز هویت
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # محاسبه مالیات مودیان
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #پایان جلسه
    dao.end_session()
    # لاگ
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

# فهرست نخ‌های مشتری
threads = []
logger = None
# کد
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
    # آن را در پیکربندی ذخیره کنید
    config["logger"] = logger
    # شروع لاگ
    logger.write("début du calcul de l'impôt des contribuables\n")
    # بازخوانی فابریکی از لایه [dao]
    dao_factory = config["layers"]["dao_factory"]
    # ایجاد یک نمونه از لایه [dao]
    dao = dao_factory.new_instance()
    # خواندن داده‌های مودی
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #برای مالیات‌دهندگان؟
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #محاسبه مالیات مودیان با استفاده از چند نخ
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #هر نخ بین ۱ تا ۴ مودی را پردازش خواهد کرد
        nb_taxpayers = min(l_taxpayers - i, random.randint(14))
        # فهرست مالیات‌دهندگان پردازش‌شده توسط نخ
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # افزایش i برای نخ بعدی
        i += nb_taxpayers
        # رشته ایجاد می‌شود
        thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
        # به فهرست نخ‌های اسکریپت اصلی اضافه می‌شود
        threads.append(thread)
        # رشته راه‌اندازی می‌شود – این عملیات ناهمزمان است – نتیجهٔ رشته منتظر نمی‌ماند
        thread.start()
    # رشتهٔ اصلی منتظر تمام رشته‌هایی که راه‌اندازی کرده تا پایان کارشان می‌ماند
    for thread in threads:
        thread.join()
    # در اینجا، همهٔ نخ‌ها کار خود را به پایان رسانده‌اند – هر یک یک یا چند شیء را تغییر داده‌اند [taxpayer]
    #نتایج در فایل jSON ذخیره می‌شوند
    dao.write_taxpayers_results(taxpayers)
    #پایان
except BaseException as erreur:
    # نمایش خطا
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # لاگ‌گیر بسته شد
    if logger:
        # پایان لاگ
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # لاگ‌گیر در حال بسته شدن است
        logger.close()
    # انجام شد
    print("Travail terminé...")
    # هر رشته‌ای را که ممکن است هنوز در حال اجرا باشد، در صورتی که فرآیند به دلیل خطا خاتمه یافته باشد، پایان دهید
    sys.exit()
  • خطوط ۴–۱۱: کلاینت منتظر پارامتری است که نوع جلسه، json یا xml، را برای استفاده با سرور مشخص می‌کند؛
  • خطوط ۱۳–۱۵: کلاینت پیکربندی شده است؛
  • خطوط ۴۸–۱۰۴: این کد آشناست. بارها استفاده شده است. این کد مالیات‌دهندگان را که باید مالیاتشان محاسبه شود، در چندین نخ (thread) توزیع می‌کند؛
  • خط ۲۶: متد [thread_function] متدی است که توسط هر ثرید برای محاسبه مالیات مودیانی که به آن اختصاص داده شده‌اند، اجرا می‌شود؛
  • خطوط ۲۷–۳۰: هر رشته لایه [dao] مخصوص به خود را دارد؛
  • محاسبه مالیات در چهار مرحله انجام می‌شود:
    • خطوط ۳۷–۳۸: آغاز یک جلسه (JSON یا XML) با سرور؛
    • خطوط ۳۹–۴۰: احراز هویت با سرور؛
    • خطوط ۴۱–۴۲: محاسبه مالیات؛
    • خطوط ۴۳–۴۴: بستن جلسه با سرور؛

وقتی این کد در حالت [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, /authentifier-utilisateur, /get-admindata, /fin-session] را آزمایش کنید:


# منتظر یک پارامتر JSON یا XML است
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()

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

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

logger = None
# کد
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
    # ذخیره آن در پیکربندی
    config["logger"] = logger
    # لاگ راه‌اندازی
    logger.write("début du calcul de l'impôt des contribuables\n")
    # بازخوانی کارخانه از لایه [dao]
    dao_factory = config['layers']['dao_factory']
    # ایجاد یک نمونه از لایه [dao]
    dao = dao_factory.new_instance()
    # بازیابی مالیات‌دهندگان
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #از مالیات‌دهندگان؟
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # نوع جلسه
    session_type = config['session_type']
    # جلسه راه‌اندازی می‌شود
    dao.init_session(session_type)
    #احراز هویت
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # استخراج داده‌ها از سازمان مالیاتی
    admindata = dao.get_admindata()
    #پایان جلسه
    dao.end_session()
    #محاسبه مالیات مودیان توسط لایه [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    #نتایج در فایل jSON ذخیره می‌شوند
    dao.write_taxpayers_results(taxpayers)
 except BaseException as erreur:
     # نمایش خطا
     print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # لاگ‌گیر بسته می‌شود
    if logger:
        # پایان لاگ
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # لاگ‌گیر بسته شد
        logger.close()
    # تکمیل شد
    print("Travail terminé...")
  • خطوط ۱–۱۱: پارامتر [json, xml] بازیابی می‌شود که نوع جلسه‌ای را که باید با سرور برقرار شود، تعیین می‌کند؛
  • خطوط ۱۳–۱۵: کلاینت پیکربندی می‌شود؛
  • خطوط ۳۰–۳۳: یک لایه [dao] ایجاد می‌شود؛
  • خطوط ۳۴–۳۵: با استفاده از این، فهرست مودیانی را که مالیات برای آن‌ها باید محاسبه شود، بازیابی می‌کنیم؛
  • سپس مراحل چهارگانه گفتگو با سرور را طی می‌کنیم؛
    • خطوط ۴۱–۴۲: یک جلسه با سرور آغاز می‌شود؛
    • خطوط ۴۳–۴۴: ما با سرور احراز هویت می‌کنیم؛
    • خطوط ۴۵–۴۶: ثابت‌های مالیاتی مورد نیاز برای محاسبه مالیات از سرور درخواست می‌شوند؛
    • خطوط ۴۷–۴۸: جلسه با سرور بسته می‌شود؛
  • خطوط ۴۹–۵۲: با استفاده از این ثابت‌ها، مالیات قابل پرداخت توسط مودیان با استفاده از لایه [métier] که محلی برای کلاینت است، محاسبه می‌شود؛
  • خطوط ۵۳–۵۴: نتایج به‌دست‌آمده ثبت می‌شوند؛

برای یک جلسه 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, /calculer-impots, /get-simulations, /delete-simulation, /fin-session] را آزمایش کنید:

Image


# منتظر پارامتر JSON یا XML
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()

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

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

logger = None
# کد
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
    # ذخیره آن در پیکربندی
    config["logger"] = logger
    # شروع ثبت گزارش
    logger.write("début du calcul de l'impôt des contribuables\n")
    # بازخوانی کارخانه از لایه [dao]
    dao_factory = config["layers"]["dao_factory"]
    # ایجاد یک نمونه از لایه [dao]
    dao = dao_factory.new_instance()
    # خواندن داده‌های مودی
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #برای مالیات‌دهندگان؟
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #محاسبه مالیات مودیان
    # تعداد مودیان
    nb_taxpayers = len(taxpayers)
    # لاگ
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # جلسه راه‌اندازی شد
    dao.init_session(session_type)
    # احراز هویت
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #محاسبه مالیات مودیان
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # درخواست فهرست شبیه‌سازی‌ها
    simulations = dao.get_simulations()
    # حذف هر شبیه‌سازی دیگر
    for i in range(len(simulations)):
        if i % 2 == 0:
            # شبیه‌سازی حذف می‌شود
            dao.delete_simulation(simulations[i]['id'])
    #پایان جلسه
    dao.end_session()
    # برای مشاهده نتایج مختلف، لاگ‌ها را بررسی کنید (حالت اشکال‌زدایی=True)
except BaseException as erreur:
    # نمایش خطا
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # بستن لاگر
    if logger:
        # پایان لاگ
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # لاگ‌گیر بسته شد
        logger.close()
    # تکمیل شد
    print("Travail terminé...")
  • خطوط ۱–۱۱: نوع جلسه از پارامترهای اسکریپت بازیابی می‌شود؛
  • خطوط ۱۳–۱۵: برنامه پیکربندی شده است؛
  • خطوط ۲۵–۵۰: این شامل کدی است که قبلاً در مقاطعی توضیح داده شده است؛
  • خطوط ۵۱–۵۲: ما فهرست شبیه‌سازی‌های انجام‌شده در جلسهٔ جاری را درخواست می‌کنیم؛
  • خطوط ۵۳–۵۷: حذف هر شبیه‌سازی دیگر؛
  • خطوط ۵۸–۵۹: جلسه خاتمه می‌یابد؛

در طول یک جلسه 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
  • خط ۶: ۱۱ شبیه‌سازی وجود دارد؛
  • خط ۱۲: پس از حذف‌های مختلف، تنها ۵ مورد باقی مانده است؛

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
        # نباید هیچ خطایی وجود داشته باشد
        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
        # نباید هیچ خطایی وجود داشته باشد
        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
        #باید خطایی وجود داشته باشد
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        # ابتدای جلسه
        dao.init_session('json')
        # آزمایش
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # نباید هیچ خطایی وجود داشته باشد
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        # راه‌اندازی جلسه
        dao.init_session('json')
        # آزمایش
        erreur = False
        try:
            dao.authenticate_user('x''y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #باید خطایی وجود داشته باشد
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        # راه‌اندازی جلسه
        dao.init_session('json')
        #احراز هویت
        dao.authenticate_user('admin''admin')
        # محاسبه مالیات
        # {'متأهل': 'بله', 'فرزندان': 2, 'حقوق': 55555,
        # 'مالیات': 2814, 'سربار': 0, 'تخفیف': 0, 'کاهش': 0, 'نرخ': 0.14}
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        #get_simulations
        simulations = dao.get_simulations()
        # بررسی‌ها
        # باید ۱ شبیه‌سازی وجود داشته باشد
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        # بررسی مالیات محاسبه شده
        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')
        # ابتدای جلسه
        dao.init_session('json')
        # احراز هویت
        dao.authenticate_user('admin''admin')
        # محاسبه مالیات
        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()
        #بررسی – دیگر نباید هیچ شبیه‌سازی‌ای وجود داشته باشد
        self.assertEqual(0, len(simulations))
        # حذف شبیه‌سازی‌ای که وجود ندارد
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # باید یک خطا وجود داشته باشد
        self.assertTrue(erreur)

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

    # لاگ
    logger = Logger(config["logsFilename"])
    # ذخیره آن در پیکربندی
    config["logger"] = logger

    #لایه [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    # روش‌های آزمون را اجرا کنید
    print("tests en cours...")
    unittest.main()
  • لایه [dao] درخواستی را به سرور ارسال می‌کند، پاسخ آن را دریافت کرده و برای بازگرداندن به کد فراخوان آن را قالب‌بندی می‌کند. هنگامی که سرور پاسخی با کد وضعیت غیر از 200 ارسال می‌کند، لایه [dao] یک استثنا پرتاب می‌کند. بنابراین تعدادی تست بررسی می‌کنند که آیا خطایی رخ داده است یا خیر؛
  • خطوط ۹–۱۸: یک جلسه jSON راه‌اندازی می‌شود. نباید خطایی وجود داشته باشد؛
  • خطوط ۲۰–۲۹: یک جلسه XML راه‌اندازی می‌شود. نباید خطایی وجود داشته باشد؛
  • خطوط ۳۱–۴۰: یک جلسه با نوع نادرست راه‌اندازی می‌شود. باید خطایی رخ دهد؛
  • خطوط ۴۲–۵۴: ما با استفاده از اعتبارنامه‌های صحیح احراز هویت می‌کنیم. نباید خطایی وجود داشته باشد؛
  • خطوط ۵۶–۶۸: ما با استفاده از اعتبارنامه‌های نادرست احراز هویت می‌کنیم. یک خطا باید رخ دهد؛
  • خطوط ۷۰–۹۲: محاسبه مالیات انجام می‌شود، سپس لیست شبیه‌سازی‌ها درخواست می‌شود. باید یک مورد وجود داشته باشد. علاوه بر این، ما بررسی می‌کنیم که این شبیه‌سازی واقعاً حاوی مالیات درخواستی است؛
  • خطوط ۹۴–۱۱۹: یک شبیه‌سازی ایجاد و سپس حذف می‌شود. سپس تلاشی برای حذف شبیه‌سازی در زمانی که دیگر هیچ شبیه‌سازی وجود ندارد، انجام می‌شود. باید خطایی رخ دهد؛
  • خطوط ۱۲۱–۱۳۷: تست به عنوان یک اسکریپت استاندارد کنسول اجرا می‌شود؛
  • خطوط ۱۲۲–۱۲۴: ما برنامه را پیکربندی می‌کنیم؛
  • خطوط ۱۲۶–۱۲۹: ما لاگ‌گیر را پیکربندی می‌کنیم. این کار به ما امکان می‌دهد تا لاگ‌ها را نظارت کنیم؛
  • خطوط ۱۳۱–۱۳۳: ما لایه [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