Skip to content

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

سنقوم بكتابة ثلاثة تطبيقات عميل تعمل على وحدة التحكم للخدمات 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 لخادم حساب الضرائب.

  • السطر 7: توسع الواجهة فئة [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

        # إذا كان رمز الحالة مختلفًا عن 200 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: منشئ الفئة؛
  • السطر 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 و [/calculer-impot] و [/authentifier-utilisateur]. في هذه الحالة، يتم استخدام المعلمة [data_value] التي تم استلامها عبر الطريقة؛
    • النوع [json]. وهذا هو الحال بالنسبة لـ URL و [/calculer-impots]. وعندئذٍ يتم استخدام المعلمة [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 أنه في كلتا الحالتين، نحصل في [résultat] على قاموس يحتوي على المفاتيح [action, état, réponse]؛

  • الأسطر 70-72: إذا كانت الإجابة تحتوي على ملفات تعريف الارتباط، يتم استردادها. وسيتعين إعادة إرسالها عند الطلب التالي؛
  • الأسطر 74-79: إذا كان حالة الرد HTTP مختلفة عن 200، يتم إثارة استثناء مع رسالة الخطأ الموجودة في نتيجة[’réponse’]. قد يكون ذلك خطأً واحداً أو قائمة من الأخطاء؛
  • الأسطر 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):
        # عنوان 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] في السطر 1؛
  • الأسطر 2-4: يتم تحديد خدمة URL المطلوب الاستعلام عنها؛
  • الأسطر 5-8: معلمات POST نظرًا لأن URL و[/authentifier-utilisateur] ينتظران POST مع معلمات [user, password]؛
  • السطر 11: يتم تنفيذ الطلب. ومرة أخرى، لا يتم استرداد الرد من الخادم. والاستثناء الذي أطلقه [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) وبالتالي تشكل نتيجة الطريقة؛
  • الأسطر 2-4: يتم تحديد خدمة URL المطلوب الاستعلام عنها؛
  • الأسطر 6-10: معلمات الخدمة التي سيتم إرسالها. في الواقع، تنتظر خدمة URL التابعة لخدمة [/calculer-impot] استلام طلب POST مع المعلمات [marié, enfants, salaire]؛
  • السطران 12-13: يتم تنفيذ الطلب واسترداد استجابة الخادم. تعيد خدمة URL التابعة لخدمة [/calculer-impot] قاموسًا يحتوي على مفاتيح الضريبة [impôt, décôte, surcôte, réduction, taux
  • السطر 15: يتم استخدام القاموس [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))

        # رابط الخدمة
        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] بنتائج الخادم
  • السطر 2: تتلقى الطريقة قائمة بالمكلفين من النوع TaxPayer؛
  • الأسطر 7-13: يتم تحويل هذه القائمة من العناصر من النوع [TaxPayer] إلى قائمة من القواميس من النوع [marié, enfants, salaire]؛
  • الأسطر 15-17: يتم تعيين URL للخدمة؛
  • الأسطر 19-20: يتم تنفيذ استعلام POST الذي يتكون نصه jSON من قائمة القواميس التي تم إنشاؤها في السطر 7. يتم استرداد الرد من الخادم؛
  • السطران 23-24: أظهرت الاختبارات وجود مشكلة عندما تكون الجلسة من النوع XML:
    • إذا كانت القائمة الأولية للمكلفين تحتوي على N عنصرًا (N>1)، نحصل كنتيجة على قائمة من N قواميس من النوع [OrderedDict]؛
    • إذا كانت القائمة الأولية تحتوي على عنصر واحد فقط، فلا نحصل على قائمة بل على عنصر من النوع [OrderedDict]؛
  • السطران 23-24: إذا كنا في الحالة الأخيرة (عنصر واحد)، فإننا نحول النتيجة إلى قائمة مكونة من عنصر واحد؛
  • الأسطر 25-28: تحتوي قائمة القواميس المستلمة هذه على ضريبة كل دافع ضرائب من القائمة الأولية. ثم يتم تحديث كل منها بالنتائج المستلمة؛

[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)
  • السطر 1: تطلب الطريقة قائمة عمليات المحاكاة التي تم إجراؤها في الجلسة الحالية؛
  • السطر 2: تعرض الطريقة استجابة الخادم؛

[delete_simulation]


    def delete_simulation(self, id: int) -> list:
        # رابط الخدمة
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

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

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

        # النتيجة عبارة عن قاموس من القيم من النوع str إذا كانت الجلسة بتنسيق 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)
  • السطر 1: تطلب هذه الطريقة من الخادم الثوابت الضريبية التي تسمح بحساب الضريبة؛
  • السطر 29: تُرجع نوعًا [AdminData]؛
  • السطر 9: يتم استرداد استجابة الخادم في شكل قاموس. تُظهر الاختبارات وجود مشكلة عندما تكون الجلسة من نوع XML: فبدلاً من أن تكون القيم أرقامًا، تكون قيم القاموس سلاسل أحرف. كنا قد أبلغنا عن هذه المشكلة أثناء دراسة الوحدة النمطية [xmltodict] ولاحظنا أن هذا هو السلوك الطبيعي. لا تحتوي الوحدة النمطية [xmltodict] على معلومات عن الأنواع في التدفق XML الذي يتم تزويدها به. ومع ذلك، في هذه الحالة بالذات، يجب تحويل جميع قيم القاموس المستلم إلى قيم رقمية. يحتوي هذا القاموس على ثلاث قوائم [limites, coeffr, coeffn] ومجموعة من الخصائص الرقمية؛
  • الأسطر 13-25: إنشاء قاموس [résultat2] بقيم رقمية انطلاقًا من القاموس [résultat] بقيم من نوع سلسلة أحرف؛
  • السطر 29: يتم استخدام القاموس [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

    # الخطوة 1 ------

    # مجلد هذا الملف
    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",
    ]

    # تحديد مسار النظام
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # الخطوة 2 ------
    # تكوين التطبيق باستخدام الثوابت
    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
    }
    )

    # الخطوة 3 ------
    # إنشاء مثيلات الطبقات
    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] في مؤشر ترابط
# «taxpayers» هي قائمة بالمكلفين
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):
        # سيعالج كل مؤشر ترابط من 1 إلى 4 دافعي ضرائب
        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()
  • الأسطر 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, /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é...")
  • الأسطر 1-11: يتم استرداد المعلمة [json, xml] التي تحدد نوع الجلسة المراد إنشاؤها مع الخادم؛
  • الأسطر 13-15: يتم تكوين العميل؛
  • الأسطر 30-33: يتم إنشاء طبقة [dao]؛
  • الأسطر 34-35: باستخدام هذه الطبقة، يتم استرداد قائمة المكلفين الذين يجب حساب الضريبة عليهم؛
  • ثم نجد الخطوات الأربع للحوار مع الخادم؛
    • الأسطر 41-42: يتم بدء جلسة مع الخادم؛
    • الأسطر 43-44: يتم إثبات الهوية لدى الخادم؛
    • السطران 45-46: نطلب من الخادم الثوابت الضريبية التي تسمح بحساب الضريبة؛
    • السطران 47-48: يتم إغلاق الجلسة مع الخادم؛
  • الأسطر 49-52: باستخدام هذه الثوابت، يمكننا حساب الضريبة المستحقة على المكلفين باستخدام الطبقة [métier] المحلية لدى العميل؛
  • السطران 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, /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()
    # راجع السجلات للاطلاع على النتائج المختلفة (وضع debug=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é...")
  • الأسطر 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
        # لا يجب أن يكون هناك أي خطأ
        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] بإطلاق استثناء. كما أن عددًا من الاختبارات يهدف إلى معرفة ما إذا كان قد حدث استثناء أم لا؛
  • الأسطر 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