Skip to content

20. تمرین عملی: نسخه ۵

Image

ما سه برنامه را توسعه خواهیم داد:

  • برنامهٔ ۱ پایگاه داده را راه‌اندازی خواهد کرد که جایگزین فایل [admindata.json] از نسخهٔ ۴ می‌شود؛
  • برنامهٔ ۲ مالیات‌ها را به‌صورت دسته‌ای محاسبه خواهد کرد؛
  • برنامهٔ ۳ مالیات‌ها را به‌صورت تعاملی محاسبه خواهد کرد؛

20.1. برنامه ۱: راه‌اندازی پایگاه داده

برنامهٔ ۱ معماری زیر را خواهد داشت:

Image

این یک تحول در معماری از نسخه ۴ (بخش |نسخه ۴|) است: داده‌های مالیاتی به جای ذخیره در فایل jSON در یک پایگاه داده ذخیره خواهند شد. لایه [dao] برای پیاده‌سازی این تغییر به‌روزرسانی خواهد شد.

20.1.1. فایل [admindata.json]

Image

فایل [admindata.json] همانند نسخه ۴ است:


{
    "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 ایجاد می‌کنیم که متعلق به کاربر [admimpots] با رمز عبور [mdpimpots] است. در [pgAdmin این منجر به موارد زیر می‌شود:

Image

پایگاه‌های داده ایجاد شده‌اند اما در حال حاضر حاوی هیچ جدولی نیستند. این جداول توسط ORM و [sqlalchemy] ایجاد خواهند شد.

20.1.3. اشیایی که توسط [sqlalchemy] نگاشت شده‌اند

ما دو جدول برای دربرگرفتن داده‌های [admindata.json] ایجاد خواهیم کرد:

جدول [tbtranches] که توسط [sqlalchemy] تعریف شده است، داده‌ها را از آرایه‌های [limites, coeffr, coeffn] در فرهنگ لغت [admindata.json] جمع‌آوری خواهد کرد:


    # جدول مقاطع مالیاتی
    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)
                           )

تعریف‌شده توسط [sqlalchemy جدول [tbconstantes] شامل ثابت‌های فرهنگ لغت [admindata.json] خواهد بود:


    # جدول ثابت‌ها
    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

واحد [Constantes] شامل ثابت‌های فرهنگ لغت [admindata.json] است:


from BaseEntity import BaseEntity


#کلاس کانتینر برای داده‌های سازمان مالیاتی
class Constantes(BaseEntity):
    #کلیدهایی که از تعریف کلاس مستثنی شده‌اند
    excluded_keys = ["_sa_instance_state"]

    # کلیدهای مجاز
    @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"]
  • خط ۵: کلاس [Constantes] از کلاس [BaseEntity] ارث می‌برد؛
  • خط ۷: از طریق نگاشت [sqlalchemy]، کلاس [Constante] ویژگی [_sa_instance_state] را دریافت خواهد کرد. ما آن را از فرهنگ لغت موجوده [asdict] حذف می‌کنیم؛
  • خطوط ۱۱–۲۳: ویژگی‌های این موجودیت. ما از نام‌های فرهنگ لغت [admindata.json] استفاده کرده‌ایم تا نوشتن کد آسان‌تر شود؛

اِنتیتی [Tranche] یک ردیف را از سه جدول [limites, coeffr, coeffn] در فرهنگ لغت [admindata.json] در بر می‌گیرد:


from BaseEntity import BaseEntity


# کلاس محفظه برای داده‌های مرجع مالیاتی
class Tranche(BaseEntity):
    #که از وضعیت کلاس مستثنی شده‌اند
    excluded_keys = ["_sa_instance_state"]

    #کلیدهای مجاز
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id", "limite", "coeffr", "coeffn"]
  • خط ۵: کلاس [Tranche] از کلاس [BaseEntity] ارث می‌برد؛
  • خط ۷: ویژگی [_sa_instance_state]، که توسط [sqlalchemy] اضافه شده است، از ویژگی‌های فرهنگ لغت موجوده [asdict] مستثنی شده است؛
  • خطوط ۱۰–۱۲: ویژگی‌های کلاس؛

نقشه بین انتیت‌های [Constantes, Tranche] و جداول [constantes, tranches] به شرح زیر خواهد بود:

Image



    # جدول ثابت‌ها
    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)
                             )

    #جدول رده‌های مالیاتی
    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)
                           )
    #نگاشت‌ها
    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
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    #رشته‌های اتصال برای پایگاه‌های داده در حال استفاده
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    #رشته اتصال برای پایگاه داده در حال استفاده
    engine = create_engine(connection_strings[config['sgbd']])

    # مِتادیتا
    metadata = MetaData()

    # جدول ثابت‌ها
    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)
                             )

    # جدول رده مالیاتی
    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)
                           )
    #نگاشت‌ها
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    #کارخانه جلسه
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    #یک جلسه
    session = session_factory()

    # اطلاعات خاصی ثبت می‌شود
    config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
                          "constantes_table": constantes_table, "session": session}

    # نتیجه
    return config
  • خط ۱: تابع [configure] یک دیکشنری را به‌عنوان پارامتر می‌پذیرد که در آن کلید [sgbd] به آن می‌گوید از کدام SGBD استفاده کند: MySQL (MySQL) یا PostgreSQL (PostgreSQL);
  • خطوط ۶–۱۲: پایگاه داده مشخص‌شده در پیکربندی انتخاب می‌شود؛
  • خطوط 14–44: نگاشت‌های موجوده/جدول. این نگاشت‌ها ساده هستند زیرا هیچ رابطه‌ای بین جدول‌های [tranches] و [constantes] وجود ندارد. آنها مستقل هستند. بنابراین هیچ کلید خارجی از یکی به دیگری برای مدیریت وجود ندارد؛
  • خطوط ۴۶–۵۱: جلسه کاری برنامه [session] ایجاد می‌شود؛
  • خطوط ۵۳–۵۸: اطلاعات مربوطه در فرهنگ لغت پیکربندی قرار داده می‌شود که سپس بازگردانده می‌شود؛

20.1.5. لایه [dao]

بیایید به معماری برنامهٔ ۱ که قرار است ساخته شود بازگردیم:

Image

لایه [dao] [1] باید فایل [admindata.json] [2] را بخواند و محتویات آن را به یکی از پایگاه‌های داده [3, 4] منتقل کند؛

Image

لایه [dao] رابط [1] را در دسترس قرار می‌دهد و توسط کلاس [2] پیاده‌سازی شده است.

رابط [InterfaceDao4TransferAdminData2Database] به شرح زیر است:


# واردات
from abc import ABC, abstractmethod


#رابط InterfaceImpôtsUI
class InterfaceDao4TransferAdminData2Database(ABC):
    #انتقال داده‌های مالیاتی به یک پایگاه داده
    @abstractmethod
    def transfer_admindata_in_database(self:object):
        pass
  • خطوط ۸–۱۰: این رابط تنها یک متد، [transfer_admindata_in_database]، را تعریف می‌کند که هیچ پارامتری ندارد. از آنجایی که این متد به پارامترهایی (کدام فایل؟ کدام پایگاه داده؟) نیاز دارد، این بدان معناست که این پارامترها به سازنده کلاس‌های پیاده‌ساز این رابط پاس داده خواهند شد؛

کلاس [DaoTransferAdminDataFromJsonFile2Database] رابط [InterfaceDao4TransferAdminData2Database] را به شرح زیر پیاده‌سازی می‌کند:


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

    # تولیدکننده
    def __init__(self, config: dict):
        self.config = config

    #انتقال
    def transfer_admindata_in_database(self) -> None:
        # ابتدایی‌سازی‌ها
        session = None
        config = self.config

        try:
            #بازیابی داده‌ها از مراجع مالیاتی
            with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
                #انتقال محتوا به یک فرهنگ لغت
                admindata = json.load(fd)

            #بازیابی پیکربندی پایگاه داده
            database = config["database"]

            # حذف دو جدول از پایگاه داده
            #checkfirst=True: ابتدا بررسی می‌کند که جدول وجود دارد
            database["tranches_table"].drop(database["engine"], checkfirst=True)
            database["constantes_table"].drop(database["engine"], checkfirst=True)

            #بازسازی جداول از روی نگاشت‌ها
            database["metadata"].create_all(database["engine"])

            # جلسهٔ جاری [sqlalchemy]
            session = database["session"]

            #جدول نرخ مالیات پر می‌شود
            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]}))
            # پر کردن جدول ثابت‌ها
            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"]
            }))

            #اعتبارسنجی جلسه [sqlalchemy]
            session.commit()
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # استثناء در شکلی متفاوت مجدداً پرتاب می‌شود
            raise ImpôtsError(17, f"{erreur}")
        finally:
            #منابع جلسه آزاد می‌شوند
            if session:
                session.close()
  • خط ۱۳: کلاس [DaoTransferAdminDataFromJsonFile2Database] رابط [InterfaceDao4TransferAdminData2Database] را پیاده‌سازی می‌کند؛
  • خطوط ۱۵–۱۷: سازنده کلاس واژهٔ پیکربندی را به‌عنوان پارامتر می‌پذیرد. کلیدهای زیر استفاده خواهند شد:
    • [admindataFilename] (خط ۲۷): نام فایل jSON حاوی داده‌های مرجع مالیاتی که باید به پایگاه داده منتقل شود؛
    • [database خط ۳۲: پیکربندی برنامه، [sqlalchemy];
  • خطوط ۳۴–۳۷: حذف جداول [constantes] و [tranches] در صورت وجود؛
  • خطوط ۳۹–۴۰: هر دو جدول را دوباره ایجاد کنید؛
  • خط ۴۳: بازیابی جلسه [sqlalchemy] موجود در پیکربندی؛
  • خطوط ۴۵–۵۱: جداول [limites, coeffr, coeffn] از فرهنگ لغت [admindata] به جلسه اضافه می‌شوند. برای این کار، نمونه‌هایی از موجودیت [Tranche] به جلسه اضافه می‌شوند؛
  • خطوط ۵۲–۶۴: یک نمونه از موجودیت [Constantes] به جلسه اضافه می‌شود؛
  • خطوط ۶۶–۶۷: جلسه اعتبارسنجی می‌شود. اگر داده‌های جلسه هنوز در پایگاه داده وجود نداشته باشد، در این نقطه وارد می‌شود؛
  • خطوط ۶۸–۷۰: رسیدگی به هرگونه خطا؛
  • خطوط ۷۱–۷۴: جلسه بسته می‌شود. این امر ممکن است زیرا لایه [dao] تنها یک بار استفاده می‌شود؛

20.1.6. پیکربندی برنامه

Image

برنامه توسط سه فایل [1] پیکربندی می‌شود:

  • [config] فایل پیکربندی عمومی است. این فایل، برنامه [main] را پیکربندی می‌کند. این فایل توسط دو فایل دیگر پشتیبانی می‌شود:
    • [config_database]، که پیش‌تر بررسی کرده‌ایم و لایه‌های ORM و [sqlalchemy] را پیکربندی می‌کند؛
    • [config_layers]، که لایه‌های برنامه را پیکربندی می‌کند؛

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


def configure(config: dict) -> dict:
    # [config] دارای کلید [sgbd] است، که عبارت است از:
    # [mysql] برای مدیریت یک پایگاه داده MySQL
    #[pgres] برای مدیریت یک پایگاه داده PostgreSQL

    import os

    # مرحلهٔ ۱ ---
    # مسیر پایتون برنامه را تنظیم کنید

    # مسیر مطلق پوشه‌ای که این اسکریپت در آن قرار دارد
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir (ممکن است نیاز به تغییر داشته باشد)
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #مسیرهای مطلق وابستگی‌ها
    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",
        #پوشه‌های محلی
        f"{script_dir}",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{script_dir}/../../entities",
    ]

    # مسیر سیستم را تنظیم کنید
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    # تکمیل پیکربندی برنامه
    config.update({
        # مسیرهای مطلق به فایل‌های داده
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json"
    })

    # مرحله ۳ ------
    # پیکربندی پایگاه داده
    import config_database
    config = config_database.configure(config)

    # مرحله ۴ ------
    # مصداق‌سازی لایه‌های برنامه
    import config_layers
    config = config_layers.configure(config)

    # اعمال پیکربندی
    return config
  • خطوط ۸–۳۶: مسیر پایتون برنامه ساخته می‌شود؛
  • خطوط ۳۸–۴۳: مسیر فایل [admindata.json] به پیکربندی اضافه می‌شود؛
  • خطوط ۴۵–۴۸: پیکربندی [sqlalchemy];
  • خطوط ۵۰–۵۳: نمونه‌سازی لایه‌های برنامه؛
  • خط ۵۶: پیکربندی عمومی بازگردانده می‌شود؛

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


def configure(config: dict) -> dict:
    # آشکارسازی لایه [dao]
    from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
    config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)

    # بازگرداندن پیکربندی
    return config
  • خطوط ۳–۴: نمونه‌سازی لایه [dao]. ما دیده‌ایم که سازنده کلاس [DaoTransferAdminDataFromJsonFile2Database] انتظار دارد فرهنگ لغت حاوی پیکربندی عمومی برنامه را به عنوان پارامتر دریافت کند؛
  • خط ۴: ارجاع به لایه [dao] به پیکربندی اضافه می‌شود؛
  • خط ۷: پیکربندی بازگردانده می‌شود؛

20.1.7. اسکریپت [main] برای برنامه

Image

Image

اسکریپت اصلی [main] به شرح زیر است:


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

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

#مسیر سیستم تنظیم شده است – اکنون می‌توان واردات را انجام داد
from ImpôtsError import ImpôtsError

# در حال بازیابی لایه [dao]
dao = config["dao"]

#کد
try:
    #انتقال داده به پایگاه داده
    dao.transfer_admindata_in_database()
except ImpôtsError as ex1:
    #خطای نمایش داده شده
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    #خطای نمایش‌داده‌شده
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # پایان
    print("Terminé...")
  • خطوط ۱–۱۰: اسکریپت منتظر یک پارامتر است. بررسی می‌کند که پارامتر موجود و صحیح باشد؛
  • خطوط ۱۲–۱۴: برنامه با ارسال نوع انتخاب‌شده SGBD به‌عنوان پارامتر پیکربندی می‌شود (عمومی، SQLAlchemy، لایه‌ها)؛
  • خطوط ۱۹–۲۰: به لایه [dao] نیاز خواهیم داشت. آن را بازیابی می‌کنیم؛
  • خط ۲۵: ما داده‌ها را به پایگاه داده منتقل می‌کنیم. تمام اطلاعات مورد نیاز متد [transfer_admindata_in_database] در خواص لایه [dao] از خط ۲۰ موجود است. این همان جایی است که از آن بازیابی خواهد کرد؛

پس از اجرا با MySQL پایه، مورد اخیر شامل عناصر زیر است (phpMyAdmin):

Image

Image

Image

در ستون [3]، می‌توانیم مقادیری را که توسط MySQL به کلید اصلی [id] اختصاص داده شده است، مشاهده کنیم. شماره‌گذاری از عدد ۱ شروع می‌شود. اسکرین‌شات بالا پس از اجرای چندین باره اسکریپت گرفته شده است.

Image

Image

با پایگاه داده PostgreSQL، نتایج به شرح زیر است:

Image

  • روی [1] کلیک راست کنید، سپس روی [2-3] کلیک کنید؛
  • در [4]، داده‌های گروه مالیاتی واقعاً موجود است؛

ما همین فرایند را برای جدول ثابت‌ها [tbconstantes] تکرار می‌کنیم:

Image

Image

Image

20.2. برنامه ۲: محاسبه مالیات به صورت دسته‌ای

Image

20.2.1. معماری

برنامه محاسبه مالیات در نسخه ۴ از معماری زیر استفاده می‌کرد:

Image

لایه [dao] رابط [InterfaceImpôtsDao] را پیاده‌سازی می‌کند. ما کلاسی ایجاد کردیم که این رابط را پیاده‌سازی می‌کند:

  • [ImpôtsDaoWithAdminDataInJsonFileکه داده‌های مالیاتی را از فایلی به نام jSON بازیابی می‌کرد. این نسخه ۳ بود؛

ما قصد داریم رابط [InterfaceImpôtsDao] را با استفاده از یک کلاس جدید به نام [ImpotsDaoWithTaxAdminDataInDatabase] پیاده‌سازی کنیم که داده‌ها را از سازمان مالیاتی از یک پایگاه داده بازیابی خواهد کرد. لایه [dao]، مانند قبل، نتایج را در یک فایل jSON می‌نویسد و داده‌های مودی را از یک فایل متنی بازیابی می‌کند. ما می‌دانیم که اگر به استفاده از رابط [InterfaceImpôtsDao] ادامه دهیم، لایه [métier] نیازی به تغییر نخواهد داشت.

معماری جدید به شرح زیر خواهد بود:

Image

20.2.2. پیکربندی برنامه

Image

فایل پیکربندی [config_database] همانند آنچه در برنامهٔ ۱ بود، بدون تغییر باقی می‌ماند. پیکربندی [config] شامل عناصر جدیدی است:


    # مرحله ۲ ------
    #پیکربندی برنامه در حال تکمیل است
    config.update({
        # مسیرهای مطلق به فایل‌های داده
        "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"
    })
  • خطوط ۶–۸: مسیرهای مطلق فایل‌های متنی مورد استفاده توسط برنامه ۲؛

پیکربندی لایه‌ها [config_layers] به شرح زیر تغییر می‌کند:


def configure(config: dict) -> dict:
    # آشکارسازی لایه DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # آشکارسازی لایه [métier]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    # ذخیره پیکربندی
    return config
  • خطوط ۳–۴: لایه [dao] اکنون توسط کلاس [ImpotsDaoWithAdminDataInDatabase] پیاده‌سازی می‌شود. این کلاس جدید است اما همان رابط [InterfaceDao] را مانند نسخه ۴ تمرین برنامه پیاده‌سازی می‌کند؛
  • خطوط ۷–۸: لایه [métier] توسط کلاس [ImpôtsMétier] پیاده‌سازی شده است. این کلاسی است که در نسخه ۴ تمرین عملی استفاده می‌شود؛

20.2.3. لایه [dao]

Image

کلاس پیاده‌سازی [ImpotsDaoWithAdminDataInDatabase] برای رابط [InterfaceImpôtsDao] به شرح زیر خواهد بود:


# واردات‌ها
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):
    # سازنده
    def __init__(self, config: dict):
        # config["taxPayersFilename"]: نام فایل متنی حاوی جزئیات مالیات‌دهندگان
        # پیکربندی ["taxPayersResultsFilename"]: نام فایل نتایج jSON
        # config["errorsFilename"]: خطاهای یافت‌شده در taxPayersFilename
        # config["database"]: پیکربندی پایگاه داده

        # ابتدایی‌سازی کلاس والد
        AbstractImpôtsDao.__init__(self, config)
        # ذخیره‌سازی پارامترها
        self.__config = config
        # داده‌های مدیریتی
        self.__admindata = None

    #پیاده‌سازی رابط
    def get_admindata(self):
        #آیا داده‌های مدیریتی ذخیره شده است؟
        if self.__admindata:
            return self.__admindata
        # یک پرس‌وجو در BD انجام می‌شود
        session = None
        config = self.__config
        try:
            #یک جلسه
            database_config = config["database"]
            session = database_config["session"]

            #جدول مقاطع مالیاتی خوانده می‌شود
            tranches = session.query(Tranche).all()

            #جدول ثابت‌ها خوانده می‌شود (فقط یک سطر)
            constantes = session.query(Constantes).first()

            # مثال 'admindata' ایجاد می‌شود
            admindata = AdminData()
            # جدول‌های محدودیت 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))
            #ثابت‌ها را به آن اضافه می‌کند
            admindata.fromdict(constantes.asdict())
            #ذخیره `admindata`
            self.__admindata = admindata
            #مقدار را بازمی‌گرداند
            return self.__admindata
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # استثناء در شکلی متفاوت دوباره پرتاب می‌شود
            raise ImpôtsError(27, f"{erreur}")
        finally:
            # پایان جلسه
            if session:
                session.close()

یادداشت‌ها

  • خط ۱۱: کلاس [ImpotsDaoWithAdminDataInDatabase] از کلاس [AbstractImpôtsDao] ارائه شده در نسخه ۴ ارث می‌برد. می‌دانیم که دومی رابط [InterfaceDao] ارائه شده در همان نسخه را پیاده‌سازی می‌کند. این تطابق با این رابط است که به ما اجازه می‌دهد لایه [métier] را بدون تغییر باقی بگذاریم؛
  • خط ۱۳: سازنده کلاس، فرهنگ لغت پیکربندی برنامه را به‌عنوان پارامتر می‌پذیرد؛
  • خط ۲۰: کلاس والد [] مقداردهی اولیه می‌شود. این کلاس به‌طور جزئی رابط [InterfaceDao] را پیاده‌سازی می‌کند:
    • [get_taxpayers_data] فایل [taxpayersdata.txt] را که حاوی داده‌های مودی است، می‌خواند؛
    • [write_taxpayers_results] نتایج را در فایل jSON [résultats.json] می‌نویسد؛
    • [get_admindata] پیاده‌سازی نشده است؛
  • خط ۲۲: پیکربندی ارسال‌شده به‌عنوان پارامترها ذخیره می‌شود؛
  • خط ۲۷: پیاده‌سازی متد [get_admindata] از رابط [InterfaceDao]:
  • خطوط ۲۸–۳۰: متد [get_admindata] داده‌ها را از سازمان مالیاتی در یک شی از نوع [AdminData] بازیابی می‌کند و این شی را در [self.__admindata] ذخیره می‌کند. اگر متد [get_admindata] چندین بار فراخوانی شود، پایگاه داده تنها در اولین بار جستجو می‌شود. در فراخوانی‌های بعدی، شیء [self.__admindata] بازگردانده می‌شود؛
  • خطوط ۳۶–۳۷: جلسه [sqlalchemy که هنگام پیکربندی برنامه توسط [config_database] ایجاد شده بود، بازیابی می‌شود؛
  • خط ۴۰: بازه‌های مالیاتی در یک لیست فراخوانی می‌شوند؛
  • خط ۴۳: ثابت‌های مورد استفاده در محاسبه مالیات بازیابی می‌شوند؛
  • خط ۴۶: یک نمونه از کلاس [AdminData] ایجاد می‌شود. توجه داشته باشید که این کلاس از [BaseEntity] ارث می‌برد؛
  • خطوط ۴۸–۵۴: آرایه‌های [limites, coeffr, coeffn] از نمونه [AdminData] مقداردهی اولیه می‌شوند؛
  • خطوط ۵۵–۵۶: سایر ویژگی‌های **[AdminData]** با ثابت‌های مورد استفاده در محاسبه مالیات مقداردهی اولیه می‌شوند. دقت شده است که ویژگی‌های کلاس‌های **[AdminData] و [Constantes]** نام‌های یکسانی داشته باشند، که این امر کد را ساده‌تر می‌کند؛
  • خطوط ۵۷–۵۸: نمونه [AdminData] در لایه [dao] ذخیره می‌شود تا بتواند در فراخوانی‌های بعدی متد [get_admindata] بازگردانده شود؛
  • خط ۶۰: مقداری که توسط کد فراخوانی‌کننده درخواست شده است بازگردانده می‌شود؛
  • خطوط ۶۱–۶۳: رسیدگی به هرگونه خطا؛
  • خطوط 64–67: تنها یک پرس‌وجو به پایگاه داده ارسال می‌شود. بنابراین جلسه [sqlalchemy] را می‌توان بست؛

20.2.4. آزمایش لایه [dao]

در نسخه ۴ این برنامه، ما یک کلاس تست برای لایه [métier] ایجاد کرده بودیم. دقیق‌تر اینکه، این کلاس هر دو لایه [métier] و [dao] را تست می‌کرد. ما این تست را مجدداً استفاده می‌کنیم تا تأیید کنیم که لایه [dao] همان‌طور که انتظار می‌رود کار می‌کند. با این حال، لایه [métier] بدون تغییر باقی می‌ماند.

Image

Image

آزمایش [TestDaoMétier] به شرح زیر است:


import unittest


class TestDaoMétier(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'متأهل': 'بله', 'فرزندان': 2, 'حقوق': 55555,
        # 'مالیات': 2814, 'سربار': 0, 'تخفیف': 0, 'کاهش': 0, 'نرخ': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        métier.calculate_tax(taxpayer, admindata)
        #تأیید
        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

        # {'ازدواج‌کرده': 'بله', 'فرزندان': 3, 'حقوق': 200000,
        # 'مالیات': 42842, 'عوارض': 17283, 'تخفیف': 0, 'کاهش': 0, 'نرخ': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        métier.calculate_tax(taxpayer, admindata)
        #بررسی‌ها
        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__':
    # انتظار می‌رود یک پارامتر MySQL یا PostgreSQL باشد
    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()

    #پیکربندی برنامه
    import config
    config = config.configure({'sgbd': sgbd})
    # لایه منطق کسب‌وکار
    métier = config['métier']
    try:
        # داده‌های مدیریتی
        admindata = config['dao'].get_admindata()
    except BaseException as ex:
        #نمایش
        print((f"L'erreur suivante s'est produite : {ex}"))
        # پایان
        sys.exit()
    # پارامتری که توسط اسکریپت دریافت می‌شود ارسال می‌گردد
    sys.argv.pop()
    # اجرای روش‌های آزمون
    print("tests en cours...")
   unittest.main()
  • ما دوباره به ۱۱ آزمایش توصیف‌شده در بخش |[métier] آزمایش لایه، نسخه ۴| نخواهیم پرداخت؛
  • خطوط ۳۷–۶۶: ما اسکریپت تست را به‌جای یک تست UnitTest به‌عنوان یک برنامهٔ عادی اجرا خواهیم کرد. خط ۶۶ جایی است که چارچوب UnitTest وارد عمل می‌شود. در تست‌های قبلی، ما از متد [setUp] برای پیکربندی اجرای هر تست استفاده کردیم. ما مجبور بودیم همین پیکربندی را ۱۱ بار تکرار کنیم، زیرا تابع [setUp] قبل از هر تست اجرا می‌شود. در اینجا، پیکربندی را تنها یک بار انجام می‌دهیم. این شامل تعریف متغیرهای سراسری [métier] در خط ۵۳ و [admindata] در خط ۵۶ است، که سپس توسط متدهای [TestDaoMétier]، برای مثال در خط ۱۲، استفاده خواهند شد؛
  • خطوط ۳۹–۴۷: اسکریپت تست منتظر یک پارامتر [mysql / pgres] است که مشخص می‌کند از پایگاه داده MySQL یا PostgreSQL استفاده شود؛
  • خطوط ۵۰–۵۱: تست پیکربندی شده است؛
  • خط ۵۳: لایه [métier] از پیکربندی بازیابی می‌شود؛
  • خط ۵۶: همین کار با لایه [dao] نیز انجام می‌شود. سپس نمونه [admindata] که داده‌های مورد نیاز برای محاسبه مالیات را در بر می‌گیرد، بازیابی می‌شود؛
  • آزمایش‌ها نشان داده‌اند که متد [unittest.main()] در خط ۶۶، پارامتر [mysql / pgres] دریافتی توسط اسکریپت را نادیده نگرفته، بلکه معنایی متفاوت به آن نسبت داده است. خط ۶۳ تضمین می‌کند که این متد دیگر هیچ پارامتری نداشته باشد؛

ما دو پیکربندی اجرای زیر را ایجاد می‌کنیم:

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
  • خطوط ۵ و ۷: هر ۱۱ تست با موفقیت انجام شد؛

شایان ذکر است که این تست‌ها تنها ۱۱ مورد محاسبه مالیات را بررسی می‌کنند. با این حال، موفقیت آن‌ها ممکن است برای اطمینان‌بخشی به ما در مورد لایه [dao] کافی باشد.

20.2.5. اسکریپت اصلی

Image

Image

اسکریپت اصلی [main] همانند نسخهٔ ۴ است:


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

# راه‌اندازی برنامه
import config
config = config.configure({'sgbd': sgbd})

#مسیر سیستم (syspath) تنظیم شده است – اکنون می‌توان importها را انجام داد
from ImpôtsError import ImpôtsError

#بازیابی لایه‌های برنامه (آنها قبلاً نمونه شده‌اند)
dao = config["dao"]
métier = config["métier"]

try:
    # استخراج مقاطع مالیاتی
    admindata = dao.get_admindata()
    # خواندن داده‌های مودی
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # از مالیات‌دهندگان؟
    if not taxpayers:
        raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #مالیات مودیان را محاسبه می‌کند
    for taxPayer in taxpayers:
        # taxPayer هم پارامتر ورودی و هم خروجی است
        # taxPayer باید اصلاح شود
        métier.calculate_tax(taxPayer, admindata)
    #نوشتن نتایج در یک فایل متنی
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    #نمایش خطا
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # تکمیل شد
    print("Travail terminé...")

یادداشت‌ها

  • خطوط ۱–۱۰: پارامتر [mysql / pgres] بازیابی می‌شود که SGBD مورد استفاده را مشخص می‌کند؛
  • خطوط ۱۲–۱۴: برنامه پیکربندی می‌شود؛
  • خطوط ۱۶–۱۷: کلاس [ImpôtsError] وارد می‌شود. این در خط ۳۸ مورد نیاز است؛
  • خطوط ۱۹–۲۱: ارجاعات به لایه‌های برنامه بازیابی می‌شوند؛
  • خط ۲۵: از لایه [dao] برای دریافت داده‌های مرجع مالیاتی پرس‌وجو می‌شود. لایه [métier] برای محاسبه مالیات به این داده‌ها نیاز دارد؛
  • خط ۲۷: داده‌های مالیات‌دهندگان (شناسه، وضعیت تأهل، فرزندان، حقوق) را در یک لیست بازیابی می‌کنیم؛
  • خطوط ۲۹–۳۰: اگر این لیست خالی باشد، یک استثنا پرتاب می‌شود؛
  • خطوط ۳۲–۳۵: محاسبه مالیات برای موارد موجود در لیست [taxpayers]؛
  • خط ۳۷: نوشتن نتایج در فایل jSON[résultats.json];
  • خطوط ۳۸–۴۰: رسیدگی به هرگونه خطا؛

برای اجرای اسکریپت، دو | پیکربندی اجرای | ایجاد می‌شوند:

Image

نتایج به‌دست‌آمده در فایل [résultats.json] مربوط به نسخهٔ ۴ است.

Image

20.3. برنامهٔ ۳: محاسبهٔ مالیات تعاملی

اکنون برنامه محاسبه مالیات به صورت تعاملی را معرفی می‌کنیم. این نسخهٔ بازسازی‌شدهٔ برنامهٔ ۲ از نسخهٔ ۴ است.

Image

Image

  • اسکریپت [main] با استفاده از متد [ui.run] از لایه [ui]، دیالوگ را با کاربر آغاز می‌کند؛
  • لایه [ui]:
    • از لایه [dao] برای بازیابی داده‌های مورد نیاز برای محاسبه مالیات استفاده می‌کند؛
    • جزئیات مودی مالیاتی را که مالیات او باید محاسبه شود، از کاربر می‌پرسد؛
    • از لایه [métier] برای انجام این محاسبه استفاده می‌کند؛

فایل [config_layers] یک لایه اضافی را ایجاد می‌کند:


def configure(config: dict) -> dict:
    # ایجاد لایه DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # آشکارسازی لایه [métier]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    # رابط کاربری
    from ImpôtsConsole import ImpôtsConsole
    config['ui'] = ImpôtsConsole(config)

    # بازگرداندن پیکربندی
    return config

کلاس [ImpôtsConsole]، خطوط ۱۱–۱۲، همانند نسخهٔ ۴ است.

اسکریپت اصلی [main] به شرح زیر است:


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

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

#مسیر سیستم پیکربندی شده است – اکنون می‌توانیم واردات را انجام دهیم
from ImpôtsError import ImpôtsError

# بازیابی لایه [ui]
ui = config["ui"]

#کد
try:
    #اجرای لایه [ui]
    ui.run()
except ImpôtsError as ex1:
    # پیام خطا نمایش داده می‌شود
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # پیام خطا نمایش داده می‌شود
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    #در همه موارد اجرا شد
    print("Travail terminé...")
  • خطوط ۱–۱۰: اسکریپت منتظر پارامتری به نام [mysql / pgres] است که مشخص می‌کند SGBD مورد استفاده کدام باشد؛
  • خطوط ۱۲–۱۴: برنامه پیکربندی می‌شود؛
  • خطوط ۱۹–۲۰: لایه [ui] از پیکربندی بازیابی می‌شود؛
  • خط ۲۵: اجرا می‌شود؛

نتایج با نتایج |نسخهٔ ۴| یکسان است. این امر غیرممکن است، زیرا تمام رابط‌ها از نسخهٔ ۴ در نسخهٔ ۵ حفظ شده‌اند.