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

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

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

فایل [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]، این منجر به موارد زیر میشود:

به همین ترتیب، همانطور که در بخش |ایجاد پایگاه داده PostgreSQL| نشان داده شده است، ما یک پایگاه داده به نام PostgreSQL ایجاد میکنیم که متعلق به کاربر [admimpots] با رمز عبور [mdpimpots] است. در [pgAdmin]، این منجر به موارد زیر میشود:

پایگاههای داده ایجاد شدهاند اما در حال حاضر حاوی هیچ جدولی نیستند. این جداول توسط 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)
)
اشیایی که به این دو جدول نگاشت میشوند به شرح زیر هستند:

واحد [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] به شرح زیر خواهد بود:

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

ما به تازگی بخشی از پیکربندی برای [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]
بیایید به معماری برنامهٔ ۱ که قرار است ساخته شود بازگردیم:

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

لایه [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. پیکربندی برنامه

برنامه توسط سه فایل [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] برای برنامه


اسکریپت اصلی [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):



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


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

- روی [1] کلیک راست کنید، سپس روی [2-3] کلیک کنید؛
- در [4]، دادههای گروه مالیاتی واقعاً موجود است؛
ما همین فرایند را برای جدول ثابتها [tbconstantes] تکرار میکنیم:



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

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

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

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

فایل پیکربندی [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]

کلاس پیادهسازی [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] بدون تغییر باقی میماند.


آزمایش [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] دریافتی توسط اسکریپت را نادیده نگرفته، بلکه معنایی متفاوت به آن نسبت داده است. خط ۶۳ تضمین میکند که این متد دیگر هیچ پارامتری نداشته باشد؛
ما دو پیکربندی اجرای زیر را ایجاد میکنیم:


اگر هر یک از این دو پیکربندی را اجرا کنیم، نتایج زیر را به دست میآوریم:
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. اسکریپت اصلی


اسکریپت اصلی [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];
- خطوط ۳۸–۴۰: رسیدگی به هرگونه خطا؛
برای اجرای اسکریپت، دو | پیکربندی اجرای | ایجاد میشوند:

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

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


- اسکریپت [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] از پیکربندی بازیابی میشود؛
- خط ۲۵: اجرا میشود؛
نتایج با نتایج |نسخهٔ ۴| یکسان است. این امر غیرممکن است، زیرا تمام رابطها از نسخهٔ ۴ در نسخهٔ ۵ حفظ شدهاند.