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

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

وهذا يمثل تطورًا في بنية الإصدار 4 (الفقرة |الإصدار 4|): ستُخزَّن البيانات الضريبية في قاعدة بيانات بدلاً من ملف jSON. وستتطور الطبقة [dao] لتنفيذ هذا التغيير.
20.1.1. الملف [admindata.json]

الملف [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]، ينتج عن ذلك ما يلي:

وبالمثل، كما تم توضيحه في الفقرة |إنشاء قاعدة بيانات PostgreSQL|، نقوم بإنشاء قاعدة بيانات PostgreSQL باسم [dbimpots-2019] مملوكة للمستخدم [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)
)
ستجمع الجدولة [tbconstantes]، التي تم تعريفها بواسطة [sqlalchemy]، الثوابت الموجودة في القاموس [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"]
- السطر 5: الفئة [Constantes] تمتد من الفئة [BaseEntity]؛
- السطر 7: من خلال التعيين [sqlalchemy]، ستحصل الفئة [Constante] على الخاصية [_sa_instance_state]. ونستبعدها من قاموس [asdict] الخاص بالكيان؛
- الأسطر 11-23: خصائص الكيان. تم استخدام الأسماء الواردة في القاموس [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"]
- السطر 5: الفئة [Tranche] تمتد من الفئة [BaseEntity]؛
- السطر 7: يتم استبعاد الخاصية [_sa_instance_state]، التي تمت إضافتها بواسطة [sqlalchemy]، من خصائص القاموس [asdict] الخاص بالكيان؛
- الأسطر 10-12: خصائص الفئة؛
سيكون التعيين بين الكيانات [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
- السطر 1: تتلقى الدالة [configure] كمعلمة قاموسًا يحدد المفتاح [sgbd] فيه نوع SGBD الذي يجب استخدامه: MySQL (mysql) أو PostgreSQL (pgres)؛
- الأسطر 6-12: يتم اختيار قاعدة البيانات المطلوبة حسب التكوين؛
- الأسطر 14-44: تعيينات الكيانات/الجداول. هذه التعيينات بسيطة لأنه لا توجد أي صلة بين الجدولين [tranches] و [constantes]. فهما مستقلان. وبالتالي، لا توجد مفتاح أجنبي لأحدهما على الآخر يتعين إدارته؛
- الأسطر 46-51: يتم إنشاء جلسة العمل [session] للتطبيق؛
- الأسطر 53-58: يتم إدراج المعلومات المفيدة في قاموس التكوين ويتم إرجاع هذا القاموس؛
20.1.5. الطبقة [dao]
لنعد إلى بنية التطبيق 1 المراد إنشاؤه:

يجب أن تقوم الطبقة [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
- الأسطر 8-10: لا تحتوي الواجهة سوى على طريقة واحدة هي [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()
- السطر 13: الفئة [DaoTransferAdminDataFromJsonFile2Database] تُنفِّذ الواجهة [InterfaceDao4TransferAdminData2Database]؛
- الأسطر 15-17: يتلقى منشئ الفئة قاموس التكوين كمعلمة. سيتم استخدام المفاتيح التالية:
- [admindataFilename] (السطر 27): اسم الملف jSON الذي يحتوي على بيانات مصلحة الضرائب المراد نقلها إلى قاعدة البيانات؛
- [database] السطر 32: تكوين التطبيق [sqlalchemy]؛
- السطور 34-37: حذف الجدولين [constantes] و [tranches] في حالة وجودهما؛
- السطران 39-40: إعادة إنشاء الجدولين؛
- السطر 43: استرداد الجلسة [sqlalchemy] الموجودة في التكوين؛
- الأسطر 45-51: يتم إدراج الجداول [limites, coeffr, coeffn] من القاموس [admindata] في الجلسة. وللقيام بذلك، يتم إدراج مثيلات الكيان [Tranche] في الجلسة؛
- الأسطر 52-64: يتم إدراج مثيل للكيان [Constantes] في الجلسة؛
- الأسطر 66-67: يتم التحقق من صحة الجلسة. إذا لم تكن بيانات الجلسة موجودة في قاعدة البيانات بعد، يتم إدخالها في ذلك الوقت؛
- الأسطر 68-70: معالجة أي خطأ محتمل؛
- الأسطر 71-74: يتم إغلاق الجلسة. يمكن القيام بذلك لأن الطبقة [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
# الخطوة 1 ---
# يتم تعيين مسار Python للتطبيق
# المسار المطلق لمجلد هذا البرنامج النصي
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)
# الخطوة 2 ------
# استكمال تكوين التطبيق
config.update({
# المسارات المطلقة لملفات البيانات
"admindataFilename": f"{script_dir}/../../data/input/admindata.json"
})
# الخطوة 3 ------
# إعداد قاعدة البيانات
import config_database
config = config_database.configure(config)
# الخطوة 4 ------
# إنشاء مثيلات طبقات التطبيق
import config_layers
config = config_layers.configure(config)
# تطبيق التكوين
return config
- الأسطر 8-36: يتم إنشاء مسار Python الخاص بالتطبيق؛
- الأسطر 38-43: يتم إدراج مسار الملف [admindata.json] في التكوين؛
- الأسطر 45-48: تكوين ملف [sqlalchemy]؛
- الأسطر 50-53: إنشاء مثيلات لطبقات التطبيق؛
- السطر 56: يتم إرجاع التكوين العام؛
الملف [config_layers] هو كما يلي:
def configure(config: dict) -> dict:
# إنشاء مثيل لطبقة [dao]
from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)
# إرجاع التكوين
return config
- الأسطر 3-4: إنشاء مثيل لطبقة [dao]. وقد رأينا أن منشئ الفئة [DaoTransferAdminDataFromJsonFile2Database] يتوقع كمعلمة قاموس التكوين العام للتطبيق؛
- السطر 4: يتم إدراج مرجع الطبقة [dao] في التكوين؛
- السطر 7: يتم إرجاع التكوين؛
20.1.7. النص البرمجي [main] الخاص بالتطبيق


النص البرمجي الرئيسي [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) - يمكن الآن إجراء عمليات الاستيراد
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é...")
- الأسطر 1-10: يتم انتظار معلمة. يتم التحقق من وجودها وصحتها؛
- الأسطر 12-14: يتم تكوين التطبيق (الإعدادات العامة، sqlalchemy، الطبقات) عن طريق تمرير نوع SGBD المختار كمعلمة؛
- الأسطر 19-20: سنحتاج إلى الطبقة [dao]. نقوم باستردادها؛
- السطر 25: يتم إجراء النقل إلى قاعدة البيانات. جميع المعلومات اللازمة لطريقة [transfer_admindata_in_database] متوفرة في خصائص الطبقة [dao] في السطر 20. ومن هناك ستقوم الطريقة باستردادها؛
بعد التنفيذ باستخدام القاعدة MySQL، تحتوي هذه القاعدة على العناصر التالية (phpMyAdmin):



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


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

- نضغط بزر الفأرة الأيمن على [1]، ثم على [2-3]؛
- في [4]، تظهر بيانات شرائح الضرائب بشكل صحيح؛
نكرر نفس الخطوات مع جدول الثوابت [tbconstantes]:



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

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

تقوم الطبقة [dao] بتنفيذ واجهة [InterfaceImpôtsDao]. وقد أنشأنا فئةً تنفذ هذه الواجهة:
- [ImpôtsDaoWithAdminDataInJsonFile] التي كانت تستخرج البيانات الضريبية من ملف jSON. كان ذلك في الإصدار 3؛
سنقوم بتنفيذ واجهة [InterfaceImpôtsDao] من خلال فئة جديدة هي [ImpotsDaoWithTaxAdminDataInDatabase] التي ستستخرج بيانات مصلحة الضرائب من قاعدة بيانات. وستقوم الطبقة [dao]، كما في السابق، بكتابة النتائج في ملف jSON وستستخرج بيانات دافعي الضرائب من ملف نصي. ونعلم أنه إذا واصلنا الالتزام بواجهة [InterfaceImpôtsDao]، فلن تكون هناك حاجة لتعديل الطبقة [métier].
وستكون البنية الجديدة كما يلي:

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

يظل ملف التكوين [config_database] كما كان في التطبيق 1. أما التكوين [config] فيتضمن عناصر جديدة:
# الخطوة 2 ------
# إكمال تكوين التطبيق
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"
})
- الأسطر 6-8: المسارات المطلقة للملفات النصية التي يستخدمها التطبيق 2؛
تتطور تكوينات الطبقات [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
- السطران 3-4: يتم الآن تنفيذ الطبقة [dao] بواسطة الفئة [ImpotsDaoWithAdminDataInDatabase]. هذه الفئة جديدة ولكنها تنفذ نفس الواجهة [InterfaceDao] الموجودة في الإصدار 4 من تمرين التطبيق؛
- السطران 7-8: يتم تنفيذ الطبقة [métier] بواسطة الفئة [ImpôtsMétier]. وهذه هي الفئة المستخدمة في الإصدار 4 من التمرين التطبيقي؛
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):
# التكوين ["taxPayersFilename"]: اسم الملف النصي للمكلفين
# التكوين["taxPayersResultsFilename"]: اسم ملف النتائج jSON
# config["errorsFilename"]: يسجل الأخطاء التي تم العثور عليها في taxPayersFilename
# config["database"]: تهيئة قاعدة البيانات
# تهيئة فئة Parent
AbstractImpôtsDao.__init__(self, config)
# تخزين المعلمات
self.__config = config
# بيانات الإدارة
self.__admindata = None
# تنفيذ الواجهة
def get_admindata(self):
# هل تم تخزين admindata؟
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()
# يتم إنشاء الجداول limtes فيها، 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()
ملاحظات
- السطر 11: ترث الفئة [ImpotsDaoWithAdminDataInDatabase] الفئة [AbstractImpôtsDao] الواردة في الإصدار 4. ومن المعروف أن هذه الأخيرة تُنفذ الواجهة [InterfaceDao] الواردة في هذا الإصدار نفسه. وإن الامتثال لهذه الواجهة هو ما يسمح لنا بعدم تغيير الطبقة [métier]؛
- السطر 13: يتلقى منشئ الفئة قاموس إعدادات التطبيق كمعلمة؛
- السطر 20: يتم تهيئة الفئة الأم []. وهي تُنفذ جزئيًا الواجهة [InterfaceDao]:
- تقوم [get_taxpayers_data] بقراءة الملف [taxpayersdata.txt] الذي يحتوي على بيانات دافعي الضرائب؛
- تقوم [write_taxpayers_results] بكتابة النتائج في الملف jSON و [résultats.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 من هذا التطبيق، قمنا بإنشاء فئة لاختبار الطبقة [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 أو 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})
# طبقة الأعمال
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()
- لن نعود إلى الاختبارات الـ 11 الموضحة في الفقرة |اختبار الطبقة [métier] الإصدار 4|؛
- الأسطر 37-66: سنقوم بتنفيذ البرنامج النصي للاختبارات كتطبيق عادي وليس كاختبار UnitTest. السطر 66 هو الذي سيقوم بتشغيل إطار العمل UnitTest. في الاختبارات السابقة، كنا نستخدم الطريقة [setUp] لتكوين تنفيذ كل اختبار. وكنا نكرر نفس التكوين 11 مرة لأن الدالة [setUp] تُنفَّذ قبل كل اختبار. هنا، نقوم بالتكوين مرة واحدة. ويتمثل ذلك في تعريف المتغيرات العامة [métier] في السطر 53، و[admindata] في السطر 56، والتي ستُستخدم لاحقًا بواسطة طرق [TestDaoMétier]، في السطر 12 على سبيل المثال؛
- الأسطر 39-47: ينتظر البرنامج النصي للاختبار معلمة [mysql / pgres] التي تشير إلى ما إذا كنا نستخدم قاعدة بيانات MySQL أو PostgreSQL؛
- الأسطر 50-51: يتم تكوين الاختبار؛
- السطر 53: يتم استرداد الطبقة [métier] من التكوين؛
- السطر 56: يتم إجراء الأمر نفسه مع الطبقة [dao]. ثم يتم استرداد المثيل [admindata] الذي يحتوي على البيانات اللازمة لحساب الضريبة؛
- أظهرت الاختبارات أن الطريقة [unittest.main()] في السطر 66 لم تتجاهل المعلمة [mysql / pgres] التي استلمها البرنامج النصي، بل أعطتها معنىً آخر. ويضمن السطر 63 أن هذه الطريقة لم تعد تحتوي على أي معلمات؛
نقوم بإنشاء تكوينين للتنفيذ:


إذا قمنا بتنفيذ إحدى هاتين التهيئتين، فسنحصل على النتائج التالية:
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. النص البرمجي الرئيسي


البرنامج النصي الرئيسي [main] هو نفسه الموجود في الإصدار 4:
# في انتظار معلمة 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 - يمكن الآن إجراء عمليات الاستيراد
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é...")
ملاحظات
- الأسطر 1-10: يتم استرداد المعلمة [mysql / pgres] التي تشير إلى SGBD المطلوب استخدامه؛
- الأسطر 12-14: يتم تكوين التطبيق؛
- الأسطر 16-17: يتم استيراد الفئة [ImpôtsError]. نحتاج إليها في السطر 38؛
- الأسطر 19-21: يتم استرداد المراجع الخاصة بطبقات التطبيق؛
- السطر 25: يتم طلب بيانات إدارة الضرائب من الطبقة [dao]. تحتاج الطبقة [métier] إليها لحساب الضريبة؛
- السطر 27: يتم استرداد البيانات (المعرف، الحالة الاجتماعية، عدد الأبناء، الراتب) الخاصة بالمكلفين في قائمة؛
- السطران 29-30: إذا كانت هذه القائمة فارغة، يتم إثارة استثناء؛
- السطور 32-35: حساب الضريبة على عناصر القائمة [taxpayers]؛
- السطر 37: كتابة النتائج في الملف jSON[résultats.json]؛
- الأسطر 38-40: معالجة أي أخطاء محتملة؛
لتنفيذ البرنامج النصي، يتم إنشاء |تكوينين للتنفيذ|:

النتائج التي تم الحصول عليها في الملف [résultats.json] هي نتائج الإصدار 4.

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


- يُطلق البرنامج النصي [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]، السطور 11-12، هي نفسها الموجودة في |الإصدار 4|.
النص البرمجي الرئيسي [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 - يمكن إجراء عمليات الاستيراد
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é...")
- السطور 1-10، ينتظر البرنامج النصي معلمة [mysql / pgres] التي تحدد SGBD المطلوب استخدامه؛
- الأسطر 12-14: يتم تكوين التطبيق؛
- الأسطر 19-20: يتم استرداد الطبقة [ui] من التكوين؛
- السطر 25: يتم تنفيذها؛
النتائج مطابقة لتلك الخاصة بـ |الإصدار 4|. ولا يمكن أن يكون الأمر خلاف ذلك، حيث تم الالتزام بجميع واجهات الإصدار 4 في الإصدار 5.