Skip to content

20. تمرين التطبيق: الإصدار 5

Image

سنقوم بتطوير ثلاثة تطبيقات:

  • التطبيق 1 سيقوم بتهيئة قاعدة البيانات التي ستحل محل ملف [admindata.json] من الإصدار 4؛
  • التطبيق 2 سيحسب الضرائب في الوضع الدفعي؛
  • التطبيق 3 سيحسب الضرائب في الوضع التفاعلي؛

20.1. التطبيق 1: تهيئة قاعدة البيانات

سيكون للتطبيق 1 البنية التالية:

Image

هذا تطور لهيكل الإصدار 4 (انظر قسم |الإصدار 4|): سيتم تخزين بيانات الضرائب في قاعدة بيانات بدلاً من ملف JSON. سيتم تحديث طبقة [DAO] لتنفيذ هذا التغيير.

20.1.1. ملف [admindata.json]

Image

ملف [admindata.json] هو نفسه كما كان في الإصدار 4:


{
    "limites": [9964, 27519, 73779, 156244, 0],
    "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
    "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
    "plafond_qf_demi_part": 1551,
    "plafond_revenus_celibataire_pour_reduction": 21037,
    "plafond_revenus_couple_pour_reduction": 42074,
    "valeur_reduc_demi_part": 3797,
    "plafond_decote_celibataire": 1196,
    "plafond_decote_couple": 1970,
    "plafond_impot_couple_pour_decote": 2627,
    "plafond_impot_celibataire_pour_decote": 1595,
    "abattement_dixpourcent_max": 12502,
    "abattement_dixpourcent_min": 437
}

سنستخدم مفاتيح هذا القاموس كأعمدة في قاعدة البيانات.

20.1.2. إنشاء قواعد البيانات

كما هو موضح في قسم |إنشاء قاعدة بيانات MySQL|، نقوم بإنشاء قاعدة بيانات MySQL باسم [dbimpots-2019] مملوكة للمستخدم [admimpots] بكلمة مرور [mdpimpots]. في [phpMyAdmin]، يبدو هذا كما يلي:

Image

وبالمثل، كما هو موضح في القسم |إنشاء قاعدة بيانات PostgreSQL|، نقوم بإنشاء قاعدة بيانات PostgreSQL باسم [dbimpots-2019] يملكها المستخدم [admimpots] بكلمة مرور [mdpimpots]. وفي [pgAdmin]، يبدو الأمر كما يلي:

Image

تم إنشاء قواعد البيانات، ولكنها لا تحتوي على أي جداول في الوقت الحالي. سيتم إنشاء هذه الجداول بواسطة ORM [sqlalchemy].

20.1.3. الكيانات التي تم تعيينها بواسطة [sqlalchemy]

سننشئ جدولين لتغليف البيانات من [admindata.json]:

الجدول [tbtranches] المحدد بواسطة [sqlalchemy] سيجمع البيانات من المصفوفات [limites, coeffr, coeffn] في قاموس [admindata.json]:


    # la table des tranches de l'impôt
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )

يتم تعريف الجدول [tbconstantes] بواسطة [sqlalchemy وسيحتوي على الثوابت الموجودة في قاموس [admindata.json]:


    # la table des constantes
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

الكيانات التي سيتم ربطها بهذين الجدولين هي كما يلي:

Image

يحتوي الكيان [Constants] على الثوابت من قاموس [admindata.json]:

from BaseEntity import BaseEntity


#  tax administration data container class
class Constantes(BaseEntity):
    #  keys excluded from class state
    excluded_keys = ["_sa_instance_state"]

    #  authorized keys
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id",
                "plafond_qf_demi_part",
                "plafond_revenus_celibataire_pour_reduction",
                "plafond_revenus_couple_pour_reduction",
                "valeur_reduc_demi_part",
                "plafond_decote_celibataire",
                "plafond_decote_couple",
                "plafond_decote_couple",
                "plafond_impot_celibataire_pour_decote",
                "plafond_impot_couple_pour_decote",
                "abattement_dixpourcent_max",
                "abattement_dixpourcent_min"]
  • السطر 5: تمتد فئة [Constants] إلى فئة [BaseEntity]؛
  • السطر 7: عبر تعيين [sqlalchemy]، ستتلقى فئة [Constante] الخاصية [_sa_instance_state]. نستبعدها من قاموس [asdict] الخاص بالكيان؛
  • الأسطر 11–23: خصائص الكيان. لقد أعيد استخدام الأسماء المستخدمة في قاموس [admindata.json] لتسهيل كتابة الكود؛

يغلف الكيان [Tranche] صفًا من المصفوفات الثلاثة [limites، coeffr، coeffn] في قاموس [admindata.json]:

from BaseEntity import BaseEntity


#  tax administration data container class
class Tranche(BaseEntity):
    #  keys excluded from class state
    excluded_keys = ["_sa_instance_state"]

    #  authorized keys
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id", "limite", "coeffr", "coeffn"]
  • السطر 5: تمتد فئة [Tranche] من فئة [BaseEntity]؛
  • السطر 7: تم استبعاد الخاصية [_sa_instance_state] التي أضافها [sqlalchemy] من قاموس [asdict] الخاص بالكيان؛
  • الأسطر 10-12: خصائص الفئة؛

سيكون التعيين بين كيانات [Constants, Slice] وجداول [constants, slices] كما يلي:

Image


    #  the constants table
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    #  tax bracket table
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    #  mappings
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)
  • يتم تعريف التعيينات في الأسطر 24–29. وقد حذفنا التعيينات بين خصائص الكيانات المعينة وجداول قاعدة البيانات. وهذا ممكن عندما تكون أسماء أعمدة الجدول هي نفسها أسماء الخصائص التي سيتم ربطها بها. ولهذا السبب، قمنا بتضمين أسماء خصائص الكيانات المعينة في الجداول. وهذا يجعل الكود أسهل في الكتابة والفهم؛

20.1.4. ملف تكوين [sqlalchemy]

Image

لقد قمنا للتو بتفصيل جزء من تكوين [sqlalchemy]. فيما يلي ملف [config_database] الكامل:

def configure(config: dict) -> dict:
    #  sqlalchemy configuration
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    #  connection chains to the databases used
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    #  connection chain to the database used
    engine = create_engine(connection_strings[config['sgbd']])

    #  metadata
    metadata = MetaData()

    #  the constants table
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    #  tax bracket table
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    #  mappings
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    #  the factory session
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    #  a session
    session = session_factory()

    #  certain information is recorded
    config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
                          "constantes_table": constantes_table, "session": session}

    #  result
    return config
  • السطر 1: تتلقى الدالة [configure] قاموسًا كمعلمة، ويحدد مفتاح [dbms] فيه نظام إدارة قواعد البيانات (DBMS) المطلوب استخدامه: MySQL (mysql) أو PostgreSQL (pgres
  • الأسطر 6–12: يتم اختيار قاعدة البيانات المحددة في التكوين؛
  • الأسطر 14–44: تعيينات الكيانات/الجداول. هذه التعيينات بسيطة لأنه لا توجد علاقة بين الجدولين [tranches] و [constantes]. فهما مستقلان. وبالتالي، لا توجد مفاتيح خارجية بينهما لتتم إدارتها؛
  • الأسطر 46-51: إنشاء جلسة عمل التطبيق [session]؛
  • الأسطر 53–58: يتم وضع المعلومات ذات الصلة في قاموس التكوين، الذي يتم إرجاعه بعد ذلك؛

20.1.5. طبقة [dao]

لنعد إلى بنية التطبيق 1 المراد بناؤه:

Image

يجب أن تقرأ طبقة [dao] [1] ملف [admindata.json] [2] وتنقل محتوياته إلى إحدى قواعد البيانات [3، 4]؛

Image

توفر طبقة [dao] الواجهة [1] ويتم تنفيذها بواسطة الفئة [2].

واجهة [InterfaceDao4TransferAdminData2Database] هي كما يلي:

#  imports
from abc import ABC, abstractmethod


#  interface InterfaceImpôtsUI
class InterfaceDao4TransferAdminData2Database(ABC):
    #  transfer tax data to a database
    @abstractmethod
    def transfer_admindata_in_database(self:object):
        pass
  • الأسطر 8–10: تحدد الواجهة طريقة واحدة فقط [transfer_admindata_in_database] بدون معلمات. وبما أن هذه الطريقة تتطلب معلمات (أي ملف؟، أي قاعدة بيانات؟)، فهذا يعني أن هذه المعلمات ستُمرر إلى مُنشئ الفئات التي تُنفذ هذه الواجهة؛

تنفذ الفئة [DaoTransferAdminDataFromJsonFile2Database] الواجهة [InterfaceDao4TransferAdminData2Database] على النحو التالي:

#  imports
import codecs
import json

from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Constantes import Constantes
from ImpôtsError import ImpôtsError
from InterfaceDao4TransferAdminData2Database import InterfaceDao4TransferAdminData2Database
from Tranche import Tranche


class DaoTransferAdminDataFromJsonFile2Database(InterfaceDao4TransferAdminData2Database):

    #  manufacturer
    def __init__(self, config: dict):
        self.config = config

    #  transfer
    def transfer_admindata_in_database(self) -> None:
        #  initializations
        session = None
        config = self.config

        try:
            #  we retrieve data from the tax authorities
            with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
                #  transfer content to a dictionary
                admindata = json.load(fd)

            #  retrieve the database configuration
            database = config["database"]

            #  delete the two tables from the database
            #  checkfirst=True: first checks that the table exists
            database["tranches_table"].drop(database["engine"], checkfirst=True)
            database["constantes_table"].drop(database["engine"], checkfirst=True)

            #  recreate tables from mappings
            database["metadata"].create_all(database["engine"])

            #  the current [sqlalchemy] session
            session = database["session"]

            #  fill in the tax bracket table
            limites = admindata["limites"]
            coeffr = admindata["coeffr"]
            coeffn = admindata["coeffn"]
            for i in range(len(limites)):
                session.add(Tranche().fromdict(
                    {"limite": limites[i], "coeffr": coeffr[i], "coeffn": coeffn[i]}))
            #  fill in the constants table
            session.add(Constantes().fromdict({
                'plafond_qf_demi_part': admindata["plafond_qf_demi_part"],
                'plafond_revenus_celibataire_pour_reduction': admindata["plafond_revenus_celibataire_pour_reduction"],
                'plafond_revenus_couple_pour_reduction': admindata["plafond_revenus_couple_pour_reduction"],
                'valeur_reduc_demi_part': admindata["valeur_reduc_demi_part"],
                'plafond_decote_celibataire': admindata["plafond_decote_celibataire"],
                'plafond_decote_couple': admindata["plafond_decote_couple"],
                'plafond_impot_celibataire_pour_decote': admindata["plafond_impot_celibataire_pour_decote"],
                'plafond_impot_couple_pour_decote': admindata["plafond_impot_couple_pour_decote"],
                'abattement_dixpourcent_max': admindata["abattement_dixpourcent_max"],
                'abattement_dixpourcent_min': admindata["abattement_dixpourcent_min"]
            }))

            #  session validation [sqlalchemy]
            session.commit()
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            #  we relaunch the exception in another form
            raise ImpôtsError(17, f"{erreur}")
        finally:
            #  session resources are released
            if session:
                session.close()
  • السطر 13: الفئة [DaoTransferAdminDataFromJsonFile2Database] تنفذ الواجهة [InterfaceDao4TransferAdminData2Database]؛
  • الأسطر 15–17: يأخذ منشئ الفئة قاموس التكوين كمعلمة. سيتم استخدام المفاتيح التالية:
    • [admindataFilename] (السطر 27): اسم ملف JSON الذي يحتوي على بيانات إدارة الضرائب المراد نقلها إلى قاعدة البيانات؛
    • [database] السطر 32: تكوين [sqlalchemy] للتطبيق؛
  • الأسطر 34-37: حذف الجداول [constants] و [brackets] في حالة وجودها؛
  • السطران 39-40: إعادة إنشاء الجدولين؛
  • السطر 43: استرداد جلسة [sqlalchemy] من التكوين؛
  • الأسطر 45–51: تُضاف المصفوفات [limits، coeffr، coeffn] من قاموس [admindata] إلى الجلسة. وللقيام بذلك، تُضاف مثيلات كيان [Tranche] إلى الجلسة؛
  • الأسطر 52–64: تتم إضافة مثيل للكيان [Constantes] إلى الجلسة؛
  • السطور 66-67: يتم التحقق من صحة الجلسة. إذا لم تكن بيانات الجلسة موجودة بعد في قاعدة البيانات، يتم إدراجها في هذه المرحلة؛
  • الأسطر 68–70: معالجة الأخطاء؛
  • الأسطر 71–74: يتم إغلاق الجلسة. وهذا ممكن لأن طبقة [dao] تُستخدم مرة واحدة فقط؛

20.1.6. تكوين التطبيق

Image

يتم تكوين التطبيق بواسطة ثلاثة ملفات [1]:

  • [config] هو ملف التكوين العام. وهو يقوم بتكوين التطبيق [main]. ويساعده في ذلك الملفان الآخران:
    • [config_databaseالذي سبق أن تناولناه والذي يقوم بتكوين ORM [sqlalchemy]؛
    • [config_layersالذي يقوم بتكوين طبقات التطبيق؛

ملف [config] هو كما يلي:

def configure(config: dict) -> dict:
    #  [config] has the key [sgbd]:
    #  [mysql] to manage a MySQL database
    #  [pgres] to manage a PostgreSQL database

    import os

    #  step 1 ---
    #  establish the application's Python Path

    #  absolute path of this script's folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root_dir (change if necessary)
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  absolute paths of dependencies
    absolute_dependencies = [
        #  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",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  local files
        f"{script_dir}",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{script_dir}/../../entities",
    ]

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

    #  step 2 ------
    #  complete application configuration
    config.update({
        #  absolute paths for data files
        "admindataFilename": f"{script_dir}/../../data/input/admindata.json"
    })

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

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

    #  return the config
    return config
  • الأسطر 8–36: إنشاء مسار Python للتطبيق؛
  • الأسطر 38–43: إضافة مسار ملف [admindata.json] إلى التكوين؛
  • الأسطر 45–48: تكوين [SQLAlchemy]؛
  • الأسطر 50–53: إنشاء مثيلات لطبقات التطبيق؛
  • السطر 56: إرجاع التكوين العام؛

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

1
2
3
4
5
6
7
def configure(config: dict) -> dict:
    #  layer instantiation [dao]
    from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
    config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)

    #  return the config
    return config
  • السطران 3-4: إنشاء مثيل لطبقة [dao]. رأينا أن منشئ فئة [DaoTransferAdminDataFromJsonFile2Database] يتوقع قاموس التكوين العام للتطبيق كمعلمة؛
  • السطر 4: تتم إضافة الإشارة إلى طبقة [dao] إلى التكوين؛
  • السطر 7: إرجاع التكوين؛

20.1.7. نص [main] الخاص بالتطبيق

Image

Image

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

#  a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#  the syspath is set up - imports can be made
from ImpôtsError import ImpôtsError

#  we recover the [dao] layer
dao = config["dao"]

#  code
try:
    #  data transfer to the database
    dao.transfer_admindata_in_database()
except ImpôtsError as ex1:
    #  error is displayed
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    #  error is displayed
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    #  end
    print("Terminé...")
  • الأسطر 1–10: ننتظر معلمة. نتحقق من وجودها وصحتها؛
  • الأسطر 12–14: نقوم بتكوين التطبيق (عام، SQLAlchemy، طبقات) عن طريق تمرير نوع DBMS المختار كمعلمة؛
  • السطور 19-20: سنحتاج إلى طبقة [dao]. نقوم باستردادها؛
  • السطر 25: نقوم بالنقل إلى قاعدة البيانات. جميع المعلومات المطلوبة بواسطة طريقة [transfer_admindata_in_database] متوفرة في خصائص طبقة [dao] من السطر 20. ومن هناك ستستردها؛

بعد تشغيل البرنامج النصي مع قاعدة بيانات MySQL، فإنه يحتوي على العناصر التالية (phpMyAdmin):

Image

Image

Image

يُظهر العمود [3] القيم التي عيّنها MySQL للمفتاح الأساسي [id]. يبدأ الترقيم من 1. تم التقاط لقطة الشاشة أعلاه بعد تشغيل البرنامج النصي عدة مرات.

Image

Image

مع قاعدة بيانات PostgreSQL، تكون النتائج كما يلي:

Image

  • انقر بزر الماوس الأيمن على [1]، ثم على [2-3]؛
  • في [4]، تظهر بيانات الشريحة الضريبية بوضوح؛

نقوم بنفس الشيء بالنسبة لجدول الثوابت [tbconstantes]:

Image

Image

Image

20.2. التطبيق 2: حساب الضريبة في الوضع الدفعي

Image

20.2.1. البنية

استخدم تطبيق حساب الضريبة في الإصدار 4 البنية التالية:

Image

تنفذ طبقة [dao] واجهة [InterfaceImpôtsDao]. قمنا بإنشاء فئة تنفذ هذه الواجهة:

  • [TaxDaoWithAdminDataInJsonFileالتي كانت تسترد بيانات الضرائب من ملف JSON. كان ذلك في الإصدار 3؛

سنقوم بتنفيذ واجهة [InterfaceImpôtsDao] باستخدام فئة جديدة [ImpotsDaoWithTaxAdminDataInDatabase] التي ستسترد بيانات إدارة الضرائب من قاعدة بيانات. ستقوم طبقة [dao]، كما في السابق، بكتابة النتائج في ملف JSON واسترداد بيانات دافعي الضرائب من ملف نصي. نحن نعلم أنه إذا واصلنا الالتزام بواجهة [InterfaceImpôtsDao]، فلن تكون هناك حاجة لتعديل طبقة [business].

وستكون البنية الجديدة على النحو التالي:

Image

20.2.2. تكوين التطبيق

Image

يظل ملف التكوين [config_database] كما كان في التطبيق 1. يتضمن ملف التكوين [config] عناصر جديدة:


    # étape 2 ------
    # on complète la configuration de l'application
    config.update({
        # chemins absolus des fichiers de données
        "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
        "taxpayersFilename": f"{script_dir}/../../data/input/taxpayersdata.txt",
        "errorsFilename": f"{script_dir}/../../data/output/errors.txt",
        "resultsFilename": f"{script_dir}/../../data/output/résultats.json"
    })
  • الأسطر 6–8: المسارات المطلقة للملفات النصية التي يستخدمها التطبيق 2؛

يتطور تكوين الطبقات [config_layers] على النحو التالي:

def configure(config: dict) -> dict:
    #  dao layer instantiation
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    #  instantiation layer [business]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    #  return the config
    return config
  • السطران 3-4: يتم الآن تنفيذ طبقة [dao] بواسطة فئة [TaxDaoWithAdminDataInDatabase]. هذه الفئة جديدة ولكنها تنفذ نفس واجهة [DaoInterface] الموجودة في الإصدار 4 من تمرين التطبيق؛
  • السطران 7-8: يتم تنفيذ طبقة [business] بواسطة فئة [ImpôtsMétier]. هذه هي الفئة المستخدمة في الإصدار 4 من تمرين التطبيق؛

20.2.3. طبقة [DAO]

ستكون فئة التنفيذ [ImpotsDaoWithAdminDataInDatabase] للواجهة [InterfaceImpôtsDao] كما يلي:

#  imports
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from Constantes import Constantes
from ImpôtsError import ImpôtsError
from Tranche import Tranche


class ImpotsDaoWithAdminDataInDatabase(AbstractImpôtsDao):
    #  manufacturer
    def __init__(self, config: dict):
        #  config["taxPayersFilename"]: name of the taxpayer text file
        #  config["taxPayersResultsFilename"]: name of the jSON results file
        #  config["errorsFilename"]: saves errors found in taxPayersFilename
        #  config["database"]: database configuration

        #  parent class initialization
        AbstractImpôtsDao.__init__(self, config)
        #  parameter memory
        self.__config = config
        #  admindata
        self.__admindata = None

    #  interface implementation
    def get_admindata(self):
        #  admindata memorized?
        if self.__admindata:
            return self.__admindata
        #  make a query in BD
        session = None
        config = self.__config
        try:
            #  a session
            database_config = config["database"]
            session = database_config["session"]

            #  read the table of tax brackets
            tranches = session.query(Tranche).all()

            #  read the constants table (1 line only)
            constantes = session.query(Constantes).first()

            #  create the admindata instance
            admindata = AdminData()
            #  we create limtes arrays, coeffR, coeffN
            limites = admindata.limites = []
            coeffr = admindata.coeffr = []
            coeffn = admindata.coeffn = []
            for tranche in tranches:
                limites.append(float(tranche.limite))
                coeffr.append(float(tranche.coeffr))
                coeffn.append(float(tranche.coeffn))
            #  we add the constants
            admindata.fromdict(constantes.asdict())
            #  admindata is memorized
            self.__admindata = admindata
            #  we return the value
            return self.__admindata
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            #  we relaunch the exception in another form
            raise ImpôtsError(27, f"{erreur}")
        finally:
            #  close session
            if session:
                session.close()

ملاحظات

  • السطر 11: ترث فئة [ImpotsDaoWithAdminDataInDatabase] من فئة [AbstractImpôtsDao] المعروضة في الإصدار 4. ونعلم أن هذه الأخيرة تنفذ واجهة [InterfaceDao] المعروضة في نفس الإصدار. إن التوافق مع هذه الواجهة هو ما يسمح لنا بإبقاء طبقة [business] دون تغيير؛
  • السطر 13: يتلقى منشئ الفئة قاموس تكوين التطبيق كمعلمة؛
  • السطر 20: يتم تهيئة الفئة الأم []. وهي تنفذ جزئيًا واجهة [InterfaceDao]:
    • [get_taxpayers_data] يقرأ ملف [taxpayersdata.txt] الذي يحتوي على بيانات دافعي الضرائب؛
    • تقوم [write_taxpayers_results] بكتابة النتائج إلى ملف JSON [results.json]؛
    • [get_admindata] غير مُنفَّذ؛
  • السطر 22: يتم تخزين التكوين الذي تم تمريره كمعلمات؛
  • السطر 27: تنفيذ طريقة [get_admindata] للواجهة [InterfaceDao]:
  • الأسطر 28-30: تسترد طريقة [get_admindata] البيانات من إدارة الضرائب إلى كائن من النوع [AdminData] وتخزن هذا الكائن في [self.__admindata]. إذا تم استدعاء طريقة [get_admindata] عدة مرات، لا يتم الاستعلام عن قاعدة البيانات عدة مرات. يتم الاستعلام عنها فقط في المرة الأولى. في الاستدعاءات اللاحقة، يتم إرجاع كائن [self.__admindata]؛
  • الأسطر 36-37: استرجاع جلسة [sqlalchemy] التي تم إنشاؤها أثناء تكوين التطبيق بواسطة [config_database]؛
  • السطر 40: نسترد شرائح الضرائب في قائمة؛
  • السطر 43: نسترد الثوابت لحساب الضريبة؛
  • السطر 46: نقوم بإنشاء مثيل لفئة [AdminData]. تذكر أنها مشتقة من [BaseEntity]؛
  • الأسطر 48-54: نقوم بتهيئة المصفوفات [limites, coeffr, coeffn] لمثيل [AdminData]؛
  • السطران 55-56: تهيئة الخصائص الأخرى لـ [AdminData] باستخدام ثوابت حساب الضرائب. حرصنا على إعطاء نفس الأسماء لخصائص فئتي [AdminData] و [Constantes]، مما يبسط الكود؛
  • السطور 57-58: يتم تخزين مثيل [AdminData] في طبقة [dao] ليتم إرجاعه خلال الاستدعاءات اللاحقة لطريقة [get_admindata]؛
  • السطر 60: يتم إرجاع القيمة المطلوبة بواسطة الكود المستدعي؛
  • الأسطر 61-63: معالجة الأخطاء؛
  • الأسطر 64-67: يتم الاستعلام عن قاعدة البيانات مرة واحدة فقط. لذلك يمكننا إغلاق جلسة [sqlalchemy]؛

20.2.4. اختبار طبقة [dao]

في الإصدار 4 من هذا التطبيق، أنشأنا فئة اختبار لطبقة [business]. وبشكل أكثر تحديدًا، اختبرت الفئة كل من طبقتي [business] و[DAO]. نحن نعيد استخدام هذا الاختبار للتحقق من أن طبقة [DAO] تعمل كما هو متوقع. ومع ذلك، تظل طبقة [business] دون تغيير.

Image

Image

اختبار [TestDaoMétier] هو كما يلي:


import unittest
 
 
class TestDaoMétier(unittest.TestCase):
 
    def test_1(self) -> None:
        from TaxPayer import TaxPayer
 
        # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
        # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        métier.calculate_tax(taxpayer, admindata)
        # vérification
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)
 
    
 
    def test_11(self) -> None:
        from TaxPayer import TaxPayer
 
        # {'marié': 'oui', 'enfants': 3, 'salaire': 200000,
        # 'impôt': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        métier.calculate_tax(taxpayer, admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
 
 
if __name__ == '__main__':
    # on attend un paramètre mysql ou pgres
    import sys
    syntaxe = f"{sys.argv[0]} mysql / pgres"
    erreur = len(sys.argv) != 2
    if not erreur:
        sgbd = sys.argv[1].lower()
        erreur = sgbd != "mysql" and sgbd != "pgres"
    if erreur:
        print(f"syntaxe : {syntaxe}")
        sys.exit()
 
    # on configure l'application
    import config
    config = config.configure({'sgbd': sgbd})
    # couche métier
    métier = config['métier']
    try:
        # admindata
        admindata = config['dao'].get_admindata()
    except BaseException as ex:
        # affichage
        print((f"L'erreur suivante s'est produite : {ex}"))
        # fin
        sys.exit()
    # on enève le paramètre reçu par le script
    sys.argv.pop()
    # on exécute les méthodes de test
    print("tests en cours...")
   unittest.main()
  • لن نعيد النظر في الاختبارات الـ 11 الموصوفة في القسم |[business] layer test version 4|؛
  • الأسطر 37–66: سنقوم بتشغيل البرنامج النصي للاختبار كتطبيق عادي بدلاً من UnitTest. السطر 66 هو ما سيطلق إطار عمل UnitTest. في الاختبارات السابقة، استخدمنا طريقة [setUp] لتكوين تنفيذ كل اختبار. كنا نكرر نفس التهيئة 11 مرة لأن دالة [setUp] تُنفَّذ قبل كل اختبار. هنا، نقوم بالتهيئة مرة واحدة. وهي تتألف من تعريف المتغيرات العالمية [business] في السطر 53 و[admindata] في السطر 56، والتي ستُستخدم بعد ذلك بواسطة طرق [TestDaoBusiness]، على سبيل المثال في السطر 12؛
  • الأسطر 39-47: يتوقع نص الاختبار معلمة [mysql / pgres] تشير إلى ما إذا كان يتم استخدام قاعدة بيانات MySQL أو PostgreSQL؛
  • السطور 50-51: يتم تكوين الاختبار؛
  • السطر 53: يتم استرداد طبقة [business] من التكوين؛
  • السطر 56: نقوم بنفس الشيء مع طبقة [dao]. ثم نسترد مثيل [admindata]، الذي يغلف البيانات اللازمة لحساب الضريبة؛
  • أظهرت الاختبارات أن الدالة [unittest.main()] في السطر 66 لم تتجاهل المعلمة [mysql / pgres] التي تم تمريرها إلى البرنامج النصي، بل أعطتها معنىً مختلفًا. ويضمن السطر 63 أن هذه الدالة لم تعد تحتوي على أي معلمات؛

نقوم بإنشاء تكوينين للتنفيذ:

Image

Image

إذا قمنا بتشغيل أي من هذين التكوينين، فسنحصل على النتائج التالية:


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/v05/tests/TestDaoMétier.py mysql
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s
 
OK
 
Process finished with exit code 0
  • السطران 5 و7: اجتازت جميع الاختبارات الـ11؛

لاحظ أن هذه الاختبارات تتحقق فقط من 11 حالة لحساب الضريبة. ومع ذلك، قد يكون نجاحها كافياً لمنحنا الثقة في طبقة [dao].

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

Image

Image

النص البرمجي الرئيسي [main] هو نفسه الموجود في الإصدار 4:

#  a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#  the syspath is set up - imports can be made
from ImpôtsError import ImpôtsError

#  retrieve application layers (already instantiated)
dao = config["dao"]
métier = config["métier"]

try:
    #  tax bracket recovery
    admindata = dao.get_admindata()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    for taxPayer in taxpayers:
        #  taxPayer is both an input and output parameteri
        #  taxPayer will be modified
        métier.calculate_tax(taxPayer, admindata)
    #  writing results to a text file
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  completed
    print("Travail terminé...")

ملاحظات

  • الأسطر 1-10: نسترد المعلمة [mysql / pgres]، التي تحدد نظام إدارة قواعد البيانات (DBMS) المطلوب استخدامه؛
  • الأسطر 12–14: يتم تكوين التطبيق؛
  • السطور 16-17: يتم استيراد فئة [ImpôtsError]. نحتاجها في السطر 38؛
  • الأسطر 19-21: نسترد المراجع إلى طبقات التطبيق؛
  • السطر 25: نطلب بيانات إدارة الضرائب من طبقة [dao]. تحتاج طبقة [business] إلى هذه البيانات لحساب الضريبة؛
  • السطر 27: نسترد بيانات دافعي الضرائب (المعرف، الحالة الاجتماعية، عدد الأبناء، الراتب) في قائمة؛
  • السطران 29-30: إذا كانت هذه القائمة فارغة، يتم إصدار استثناء؛
  • السطور 32-35: حساب الضريبة للبنود الموجودة في قائمة [taxpayers]؛
  • السطر 37: كتابة النتائج في ملف JSON [results.json]؛
  • الأسطر 38-40: معالجة أي أخطاء؛

لتشغيل البرنامج النصي، نقوم بإنشاء |تكوينات تنفيذ|:

Image

النتائج التي تم الحصول عليها في ملف [results.json] هي تلك الموجودة في الإصدار 4.

Image

20.3. التطبيق 3: حساب الضريبة في الوضع التفاعلي

نقدم الآن التطبيق الذي يسمح بحساب الضريبة التفاعلي. هذا هو نسخة من التطبيق 2 من الإصدار 4.

Image

Image

  • يبدأ البرنامج النصي [main] حوار المستخدم باستخدام طريقة [ui.run] الخاصة بطبقة [ui]؛
  • طبقة [ui]:
    • تستخدم طبقة [dao] لاسترداد البيانات اللازمة لحساب الضريبة؛
    • تطلب من المستخدم معلومات تتعلق بالمكلف الذي سيتم حساب الضريبة عليه؛
    • تستخدم طبقة [business] لإجراء هذا الحساب؛

يقوم ملف [config_layers] بإنشاء مثيل لطبقة إضافية:

def configure(config: dict) -> dict:
    #  dao layer instantiation
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    #  instantiation layer [business]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    #  ui
    from ImpôtsConsole import ImpôtsConsole
    config['ui'] = ImpôtsConsole(config)

    #  return the config
    return config

فئة [ImpôtsConsole]، الأسطر 11–12، هي نفسها الموجودة في |الإصدار 4|.

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

#  a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#  syspath is configured - imports can be made
from ImpôtsError import ImpôtsError

#  we recover the [ui] layer
ui = config["ui"]

#  code
try:
    #  execution of the [ui] layer
    ui.run()
except ImpôtsError as ex1:
    #  the error message is displayed
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    #  the error message is displayed
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    #  executed in all cases
    print("Travail terminé...")
  • الأسطر 1-10: يتوقع البرنامج النصي معلمة [mysql / pgres] تحدد نظام إدارة قواعد البيانات (DBMS) المطلوب استخدامه؛
  • الأسطر 12-14: يتم تكوين التطبيق؛
  • السطور 19-20: يتم استرداد طبقة [ui] من التكوين؛
  • السطر 25: يتم تنفيذها؛

النتائج مطابقة لتلك الخاصة بـ |الإصدار 4|. ولا يمكن أن يكون الأمر خلاف ذلك، حيث تم الحفاظ على جميع واجهات الإصدار 4 في الإصدار 5.