20. Uygulama alıştırması: sürüm 5

Üç uygulama geliştireceğiz:
- Uygulama 1, sürüm 4'teki [admindata.json] dosyasının yerini alacak veritabanını başlatacak;
- uygulama 2, vergi hesaplamasını toplu iş modunda gerçekleştirecek;
- Uygulama 3, vergileri etkileşimli modda hesaplayacaktır;
20.1. Uygulama 1: veritabanının başlatılması
Uygulama 1 şu mimariye sahip olacaktır:

Bu, sürüm 4'ün mimarisinin (|Version 4| paragrafı) bir geliştirilmesidir: vergi verileri, jSON dosyasında değil, bir veritabanında bulunacaktır. [dao] katmanı, bu değişikliği uygulamak üzere güncellenecektir.
20.1.1. [admindata.json] dosyası

[admindata.json] dosyası, sürüm 4'teki haliyle şöyledir:
{
"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
}
Veritabanının sütunları olarak bu sözlüğün anahtarlarını kullanacağız.
20.1.2. Veritabanlarının oluşturulması
|création d’une base de données MySQL| paragrafında gösterildiği gibi, MySQL adlı bir veritabanı oluşturuyoruz; bu veritabanının adı [dbimpots-2019], sahibi [admimpots] ve şifresi [mdpimpots]'tir. [phpMyAdmin]'te bu, şu sonucu verir:

Aynı şekilde, |création d’une base de données PostgreSQL| paragrafında gösterildiği gibi, PostgreSQL adlı bir veritabanı oluşturuyoruz; bu veritabanının adı [dbimpots-2019], sahibi [admimpots] ve şifresi [mdpimpots]'tir. [pgAdmin]'te bu, aşağıdaki gibi görünür:

Veritabanları oluşturuldu ancak şimdilik hiçbir tablo içermiyorlar. Bu tablolar ORM ve [sqlalchemy] tarafından oluşturulacak.
20.1.3. [sqlalchemy] tarafından eşlenen varlıklar
[admindata.json] verilerini barındırmak üzere iki tablo oluşturacağız:
[sqlalchemy] tarafından tanımlanan [tbtranches] tablosu, [admindata.json] sözlüğündeki [limites, coeffr, coeffn] tablolarının verilerini bir araya getirecektir:
# vergi dilimleri tablosu
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] tarafından tanımlanan [tbconstantes] tablosu, [admindata.json] sözlüğündeki sabitleri bir araya getirecektir:
# sabitler tablosu
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)
)
Bu iki tablo ile eşleştirilecek varlıklar şunlardır:

[Constantes] varlığı, [admindata.json] sözlüğündeki sabitleri kapsar:
from BaseEntity import BaseEntity
# vergi idaresi verilerinin konteyner sınıfı
class Constantes(BaseEntity):
# sınıf durumundan hariç tutulan anahtarlar
excluded_keys = ["_sa_instance_state"]
# izin verilen anahtarlar
@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"]
-
- satır: [Constantes] sınıfı, [BaseEntity] sınıfını genişletir;
-
- satır: [sqlalchemy] eşlemesi sayesinde, [Constante] sınıfı [_sa_instance_state] özelliğini alacaktır. Bunu, varlığın [asdict] sözlüğünden hariç tutuyoruz;
- satır 11-23: varlığın özellikleri. Kod yazımını kolaylaştırmak için [admindata.json] sözlüğünde kullanılan isimler kullanılmıştır;
[Tranche] varlığı, [admindata.json] sözlüğündeki üç [limites, coeffr, coeffn] tablosundan bir satırı kapsular:
from BaseEntity import BaseEntity
# vergi idaresi verilerini içeren sınıf
class Tranche(BaseEntity):
# sınıf durumundan hariç tutulan anahtarlar
excluded_keys = ["_sa_instance_state"]
# izin verilen anahtarlar
@staticmethod
def get_allowed_keys() -> list:
return ["id", "limite", "coeffr", "coeffn"]
-
- satır: [Tranche] sınıfı, [BaseEntity] sınıfını genişletir;
-
- satır: [sqlalchemy] tarafından eklenen [_sa_instance_state] özelliği, varlığın [asdict] sözlüğündeki özelliklerinden çıkarılır;
- 10-12. satırlar: sınıfın özellikleri;
[Constantes, Tranche] varlıkları ile [constantes, tranches] tabloları arasındaki eşleme şu şekilde olacaktır:

…
# sabitler tablosu
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)
)
# vergi dilimleri tablosu
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)
)
# eşlemeler
from Tranche import Tranche
mapper(Tranche, tranches_table)
from Constantes import Constantes
mapper(Constantes, constantes_table)
- eşlemeler 24-29. satırlarda yapılmıştır. Burada, eşlenen varlıkların özellikleri ile veritabanı tabloları arasındaki eşleştirmeler yapılmamıştır. Bu, tablo sütunlarının adları, eşleştirilmeleri gereken özelliklerin adlarıyla aynı olduğunda mümkündür. Bu nedenle, eşlenen varlıkların özellik adlarını tablolara da ekledik. Bu, kodun yazılmasını ve anlaşılmasını kolaylaştırır;
20.1.4. [sqlalchemy] yapılandırma dosyası

Az önce [sqlalchemy] yapılandırmasının bir kısmını ayrıntılı olarak açıkladık. [config_database] dosyasının tamamı şu şekildedir:
def configure(config: dict) -> dict:
# SQLAlchemy yapılandırması
from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
from sqlalchemy.orm import mapper, sessionmaker
# kullanılan veritabanlarına bağlantı dizeleri
connection_strings = {
'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
}
# kullanılan veritabanına bağlantı dizesi
engine = create_engine(connection_strings[config['sgbd']])
# meta veriler
metadata = MetaData()
# sabitler tablosu
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)
)
# vergi dilimleri tablosu
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)
)
# eşlemeler
from Tranche import Tranche
mapper(Tranche, tranches_table)
from Constantes import Constantes
mapper(Constantes, constantes_table)
# oturum fabrikası
session_factory = sessionmaker()
session_factory.configure(bind=engine)
# bir oturum
session = session_factory()
# bazı bilgiler kaydedilir
config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
"constantes_table": constantes_table, "session": session}
# sonuç
return config
-
- satır: [configure] işlevi, parametre olarak bir sözlük alır; bu sözlüğün [sgbd] anahtarı, hangi SGBD işlevinin kullanılacağını belirtir: MySQL (mysql) veya PostgreSQL (pgres);
- 6-12. satırlar: yapılandırmada belirtilen veritabanı seçilir;
- 14-44. satırlar: varlıklar / tablolar arası eşlemeler. Bu eşlemeler basittir çünkü [tranches] ve [constantes] tabloları arasında hiçbir bağlantı yoktur. Bu tablolar birbirinden bağımsızdır. Dolayısıyla, birinin diğerine ait yabancı anahtarları yönetmek gerekmez;
- satır 46-51: Uygulamanın çalışma oturumu olan [session] oluşturulur;
- satır 53-58: gerekli bilgiler yapılandırma sözlüğüne eklenir ve sözlük döndürülür;
20.1.5. [dao] katmanı
Oluşturulacak Uygulama 1’in mimarisine geri dönelim:

[dao] [1] katmanı, [admindata.json] [2] dosyasını okumalı ve içeriğini [3, 4] veritabanlarından birine aktarmalıdır;

[dao] katmanı, [1] arayüzünü sunar ve [2] sınıfı tarafından uygulanır.
[InterfaceDao4TransferAdminData2Database] arayüzü şu şekildedir:
# ithalatlar
from abc import ABC, abstractmethod
# arayüz InterfaceImpôtsUI
class InterfaceDao4TransferAdminData2Database(ABC):
# vergi verilerinin bir veritabanına aktarılması
@abstractmethod
def transfer_admindata_in_database(self:object):
pass
- 8-10. satırlar: Arayüz, parametresiz tek bir [transfer_admindata_in_database] yöntemi sunar. Bu yöntemin parametrelere ihtiyacı olduğundan (hangi dosya?, hangi veritabanı?), bu parametrelerin bu arayüzü uygulayan sınıfların oluşturucusuna aktarılacağı anlamına gelir;
[DaoTransferAdminDataFromJsonFile2Database] sınıfı, [InterfaceDao4TransferAdminData2Database] arayüzünü şu şekilde uygular:
# ithalat
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):
# üretici
def __init__(self, config: dict):
self.config = config
# aktarım
def transfer_admindata_in_database(self) -> None:
# başlatma işlemleri
session = None
config = self.config
try:
# vergi idaresinden verilerin alınması
with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
# içeriğin bir sözlüğe aktarılması
admindata = json.load(fd)
# veritabanı yapılandırmasının alınması
database = config["database"]
# veritabanındaki iki tablonun silinmesi
# checkfirst=True: önce tablonun varlığını kontrol eder
database["tranches_table"].drop(database["engine"], checkfirst=True)
database["constantes_table"].drop(database["engine"], checkfirst=True)
# eşlemelerden tabloların yeniden oluşturulması
database["metadata"].create_all(database["engine"])
# geçerli [sqlalchemy] oturumu
session = database["session"]
# vergi dilimleri tablosu doldurulur
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]}))
# sabitler tablosu dolduruluyor
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"]
}))
# oturum doğrulama [sqlalchemy]
session.commit()
except (IntegrityError, DatabaseError, InterfaceError) as erreur:
# istisna başka bir biçimde yeniden tetiklenir
raise ImpôtsError(17, f"{erreur}")
finally:
# oturum kaynakları serbest bırakılıyor
if session:
session.close()
-
- satır: [DaoTransferAdminDataFromJsonFile2Database] sınıfı, [InterfaceDao4TransferAdminData2Database] arayüzünü uygular;
- 15-17. satırlar: Sınıfın oluşturucusu, yapılandırma sözlüğünü parametre olarak alır. Aşağıdaki anahtarlar kullanılacaktır:
- [admindataFilename] (satır 27): veritabanına aktarılacak vergi idaresi verilerini içeren jSON dosyasının adı;
- [database] satır 32: uygulamanın [sqlalchemy] yapılandırması;
- satır 34-37: [constantes] ve [tranches] tablolarının, varsa silinmesi;
- satır 39-40: iki tablonun yeniden oluşturulması;
-
- satır: yapılandırmada bulunan [sqlalchemy] oturumu alınır;
- satır 45-51: [admindata] sözlüğündeki [limites, coeffr, coeffn] tabloları oturuma eklenir. Bunun için [Tranche] varlığının örnekleri oturuma eklenir;
- satır 52-64: [Constantes] varlığının bir örneği oturuma eklenir;
- 66-67. satırlar: oturum onaylanır. Oturum verileri henüz veritabanında değilse, bu aşamada veritabanına eklenir;
- satır 68-70: Olası bir hatanın yönetimi;
- satır 71-74: oturum kapatılır. [dao] katmanı yalnızca bir kez kullanıldığından bu işlem yapılabilir;
20.1.6. Uygulamanın yapılandırılması

Uygulama, üç [1] dosyası ile yapılandırılır:
- [config] genel yapılandırma dosyasıdır. [main] uygulamasını yapılandıran bu dosyadır. Bu dosya, incelediğimiz ve ORM dosyasını yapılandıran diğer iki dosyanın da yardımını alır:
- Daha önce incelediğimiz ve ORM ile [sqlalchemy] dosyalarını yapılandıran [config_database];
- uygulamanın katmanlarını yapılandıran [config_layers];
[config] dosyası şu şekildedir:
def configure(config: dict) -> dict:
# [config], değeri şu olan [sgbd] anahtarına sahiptir:
# [mysql], MySQL veritabanını yönetmek için
# [pgres], PostgreSQL veritabanını yönetmek için
import os
# 1. adım ---
# uygulamanın Python yolunu ayarlıyoruz
# bu komut dosyasının klasörünün mutlak yolu
script_dir = os.path.dirname(os.path.abspath(__file__))
# root_dir (gerekirse değiştirin)
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# bağımlılıkların mutlak yolları
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",
# yerel klasörler
f"{script_dir}",
f"{script_dir}/../../interfaces",
f"{script_dir}/../../services",
f"{script_dir}/../../entities",
]
# syspath ayarlanıyor
from myutils import set_syspath
set_syspath(absolute_dependencies)
# 2. adım ------
# uygulama yapılandırması tamamlanıyor
config.update({
# veri dosyalarının mutlak yolları
"admindataFilename": f"{script_dir}/../../data/input/admindata.json"
})
# adım 3 ------
# veritabanı yapılandırması
import config_database
config = config_database.configure(config)
# 4. adım ------
# uygulama katmanlarının örneklenmesi
import config_layers
config = config_layers.configure(config)
# yapılandırmayı geri yükleme
return config
- 8-36. satırlar: Uygulamanın Python Yolu oluşturulur;
- 38-43. satırlar: [admindata.json] dosyasının yolu yapılandırmaya eklenir;
- 45-48. satırlar: [sqlalchemy] yapılandırması;
- 50-53. satırlar: Uygulamanın katmanları oluşturulur;
- satır 56: genel yapılandırma geri verilir;
[config_layers] dosyası şöyledir:
def configure(config: dict) -> dict:
# [dao] katmanının örneklenmesi
from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)
# yapılandırma döndürülüyor
return config
- satır 3-4: [dao] katmanının örneklenmesi. [DaoTransferAdminDataFromJsonFile2Database] sınıfının oluşturucusunun, parametre olarak uygulamanın genel yapılandırma sözlüğünü beklediğini görmüştük;
-
- satır: [dao] katmanına ilişkin referans yapılandırmaya eklenir;
-
- satır: yapılandırma geri verilir;
20.1.7. Uygulamanın [main] komut dosyası


Ana komut dosyası [main] şu şekildedir:
# mysql veya pgres parametresi bekleniyor
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()
# uygulama yapılandırılıyor
import config
config = config.configure({'sgbd': sgbd})
# syspath belirlendi - içe aktarmalar yapılabilir
from ImpôtsError import ImpôtsError
# katman alınır [dao]
dao = config["dao"]
# kod
try:
# verilerin veritabanına aktarılması
dao.transfer_admindata_in_database()
except ImpôtsError as ex1:
# hata mesajı görüntüleniyor
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# hata görüntüleniyor
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# son
print("Terminé...")
- 1-10. satırlar: bir parametre beklenir. Parametrenin mevcut ve doğru olup olmadığı kontrol edilir;
- 12-14. satırlar: Seçilen SGBD türünü parametre olarak geçirerek uygulamayı (genel, SQLAlchemy, katmanlar) yapılandırıyoruz;
- 19-20. satırlar: [dao] katmanına ihtiyacımız olacak. Bu katmanı alıyoruz;
-
- satır: Veritabanına aktarım yapılır. [transfer_admindata_in_database] yöntemine gerekli tüm bilgiler, 20. satırdaki [dao] katmanının özelliklerinde mevcuttur. Yöntem, bu bilgileri buradan alacaktır;
MySQL veritabanıyla çalıştırıldıktan sonra, veritabanı aşağıdaki öğeleri içerir (phpMyAdmin):



[3] sütununda, MySQL tarafından birincil anahtar [id]'e atanan değerler görülmektedir. Numaralandırma 1'den başlar. Yukarıdaki ekran görüntüsü, komut dosyasının birkaç kez çalıştırılmasından sonra elde edilmiştir.


PostgreSQL veritabanında sonuçlar şu şekildedir:

- [1] üzerine sağ tıklayıp ardından [2-3]'e tıklayın;
- [4]'te vergi dilimlerine ait veriler açıkça görülüyor;
Aynı işlemi [tbconstantes] sabit tablosu için de tekrarlayalım:



20.2. Uygulama 2: Toplu iş modunda vergi hesaplaması

20.2.1. Mimari
Sürüm 4'teki vergi hesaplama uygulaması aşağıdaki mimariyi kullanıyordu:

[dao] katmanı, [InterfaceImpôtsDao] arayüzünü uygular. Bu arayüzü uygulayan bir sınıf oluşturduk:
- [ImpôtsDaoWithAdminDataInJsonFile]; bu sınıf, vergi verilerini jSON dosyasından alıyordu. Bu, sürüm 3’tü;
[InterfaceImpôtsDao] arayüzünü, vergi idaresine ait verileri bir veritabanından alacak olan yeni bir [ImpotsDaoWithTaxAdminDataInDatabase] sınıfı aracılığıyla uygulayacağız. [dao] katmanı, daha önce olduğu gibi, sonuçları bir jSON dosyasına yazacak ve vergi mükelleflerinin verilerini bir metin dosyasından alacaktır. [InterfaceImpôtsDao] arayüzüne uymaya devam edersek, [métier] katmanının değiştirilmesine gerek kalmayacağını biliyoruz.
Yeni mimari şu şekilde olacaktır:

20.2.2. Uygulama yapılandırması

[config_database] yapılandırma dosyası, Uygulama 1’deki haliyle kalır. [config] yapılandırması ise yeni öğeler içerir:
# 2. adım ------
# uygulama yapılandırması tamamlanıyor
config.update({
# veri dosyalarının mutlak yolları
"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. satırlar: Uygulama 2 tarafından kullanılan metin dosyalarının mutlak yolları;
[config_layers] katman yapılandırması şu şekilde değişmektedir:
def configure(config: dict) -> dict:
# DAO katmanının örneklenmesi
from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)
# [métier] katmanının örneklenmesi
from ImpôtsMétier import ImpôtsMétier
config['métier'] = ImpôtsMétier()
# yapılandırma geri yükleniyor
return config
- 3-4. satırlar: [dao] katmanı artık [ImpotsDaoWithAdminDataInDatabase] sınıfı tarafından uygulanmaktadır. Bu sınıf yenidir ancak uygulama alıştırmasının 4. sürümünde olduğu gibi aynı [InterfaceDao] arayüzünü uygulamaktadır;
- 7-8. satırlar: [métier] katmanı, [ImpôtsMétier] sınıfı tarafından uygulanmaktadır. Bu, uygulama alıştırmasının 4. sürümünde kullanılan sınıftır;
20.2.3. [dao] katmanı

[InterfaceImpôtsDao] arayüzünün [ImpotsDaoWithAdminDataInDatabase] uygulama sınıfı şu şekilde olacaktır:
# ithalatlar
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):
# yapıcı
def __init__(self, config: dict):
# config["taxPayersFilename"] : vergi mükelleflerinin bulunduğu metin dosyasının adı
# yapılandırma["taxPayersResultsFilename"] : sonuçların jSON dosya adı
# config["errorsFilename"] : taxPayersFilename dosyasında bulunan hataları kaydeder
# config["database"] : veritabanı yapılandırması
# Parent sınıfının başlatılması
AbstractImpôtsDao.__init__(self, config)
# parametre kaydetme
self.__config = config
# admindata
self.__admindata = None
# arayüzün uygulanması
def get_admindata(self):
# admindata kaydedildi mi?
if self.__admindata:
return self.__admindata
# BD'te bir sorgu yapılıyor
session = None
config = self.__config
try:
# bir oturum
database_config = config["database"]
session = database_config["session"]
# vergi dilimleri tablosu okunuyor
tranches = session.query(Tranche).all()
# sabitler tablosu okunuyor (tek satır)
constantes = session.query(Constantes).first()
# admindata örneği oluşturuluyor
admindata = AdminData()
# burada sınır tabloları oluşturulur: 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))
# buraya sabitler eklenir
admindata.fromdict(constantes.asdict())
# admindata'yı kaydediyoruz
self.__admindata = admindata
# değeri döndürülür
return self.__admindata
except (IntegrityError, DatabaseError, InterfaceError) as erreur:
# istisnayı başka bir biçimde yeniden tetiklenir
raise ImpôtsError(27, f"{erreur}")
finally:
# oturumu kapatıyoruz
if session:
session.close()
Notlar
-
- satır: [ImpotsDaoWithAdminDataInDatabase] sınıfı, sürüm 4'te sunulan [AbstractImpôtsDao] sınıfından miras almaktadır. Bu sınıfın, aynı sürümde sunulan [InterfaceDao] arayüzünü uyguladığı bilinmektedir. Bu arayüze uyum sağlanması sayesinde [métier] katmanını değiştirmemize gerek kalmaz;
-
- satır: Sınıfın oluşturucusu, parametre olarak uygulama yapılandırma sözlüğünü alır;
-
- satır: üst sınıf [] başlatılır. Bu sınıf, [InterfaceDao] arayüzünü kısmen uygular:
- [get_taxpayers_data], vergi mükelleflerinin verilerini içeren [taxpayersdata.txt] dosyasını okur;
- [write_taxpayers_results], sonuçları jSON ve [résultats.json] dosyalarına yazar;
- [get_admindata] dosyası uygulanmamıştır;
-
- satır: parametre olarak geçirilen yapılandırma kaydedilir;
-
- satır: [InterfaceDao] arayüzünün [get_admindata] yönteminin uygulanması:
- 28-30. satırlar: [get_admindata] yöntemi, vergi idaresinden gelen verileri [AdminData] türünde bir nesneye alır ve bu nesneyi [self.__admindata]'te saklar. [get_admindata] yöntemi birden fazla kez çağrılırsa, veritabanına birden fazla kez sorgu yapılmaz. Veritabanına yalnızca ilk seferde sorgu yapılır. Sonraki seferlerde [self.__admindata] nesnesi döndürülür;
- 36-37. satırlar: [config_database] tarafından uygulamanın yapılandırılması sırasında oluşturulan [sqlalchemy] oturumu alınır;
- satır 40: vergi dilimlerini bir listeye alınır;
-
- satır: Vergi hesaplaması sabitleri alınır;
- satır 46: [AdminData] sınıfının bir örneği oluşturulur. Bu sınıfın [BaseEntity] sınıfından türediği hatırlatılmalıdır;
- 48-54. satırlar: [AdminData] örneğinin [limites, coeffr, coeffn] dizileri başlatılır;
- 55-56. satırlar: [AdminData] sınıfının diğer özellikleri, vergi hesaplamasına ait sabitlerle başlatılır. [AdminData] ve [Constantes] sınıflarının özelliklerine aynı isimler verilmesine özen gösterilmişti; bu da kodu basitleştirir;
- satır 57-58: [AdminData] örneği, [dao] katmanında saklanır; böylece [get_admindata] yöntemine yapılacak sonraki çağrılarda geri döndürülebilir;
-
- satır: çağıran kod tarafından istenen değer döndürülür;
- satır 61-63: Olası bir hatanın yönetimi;
- 64-67. satırlar: Veritabanına yalnızca tek bir sorgu gönderilir. Dolayısıyla [sqlalchemy] oturumu kapatılabilir;
20.2.4. [dao] katmanının testi
Bu uygulamanın 4. sürümünde, [métier] katmanını test eden bir sınıf oluşturmuştuk. Daha doğrusu, bu sınıf hem [métier] hem de [dao] katmanlarını test ediyordu. [dao] katmanının beklendiği gibi çalıştığını doğrulamak için bu testi tekrar kullanıyoruz. Nitekim, [métier] katmanında herhangi bir değişiklik yapılmamıştır.


[TestDaoMétier] testi şu şekildedir:
import unittest
class TestDaoMétier(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'evli': 'evet', 'çocuk': 2, 'maaş': 55555,
# 'vergi': 2814, 'ek vergi': 0, 'indirim': 0, 'azaltma': 0, 'oran': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
métier.calculate_tax(taxpayer, admindata)
# doğrulama
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
# {'evli': 'evet', 'çocuk': 3, 'maaş': 200000,
# 'vergi': 42842, 'ek ücret': 17283, 'indirim': 0, 'azaltma': 0, 'oran': 0.41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
métier.calculate_tax(taxpayer, admindata)
# kontroller
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 veya pgres parametresi bekleniyor
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()
# uygulama yapılandırılıyor
import config
config = config.configure({'sgbd': sgbd})
# iş mantığı katmanı
métier = config['métier']
try:
# admindata
admindata = config['dao'].get_admindata()
except BaseException as ex:
# görüntüleme
print((f"L'erreur suivante s'est produite : {ex}"))
# son
sys.exit()
# komut dosyası tarafından alınan parametreyi silinir
sys.argv.pop()
# test yöntemleri çalıştırılır
print("tests en cours...")
unittest.main()
- |[métier] katmanı, sürüm 4 testi| başlıklı paragrafta açıklanan 11 teste tekrar değinmeyeceğiz;
- 37-66. satırlar: Test komut dosyasını bir UnitTest testi olarak değil, normal bir uygulama gibi çalıştıracağız. UnitTest çerçevesini devreye sokacak olan 66. satırdır. Önceki testlerde, her bir testin yürütülmesini yapılandırmak için [setUp] yöntemini kullanıyorduk. [setUp] işlevi her testten önce çalıştırıldığı için aynı yapılandırmayı 11 kez tekrarlıyorduk. Burada ise yapılandırmayı sadece 1 kez yapıyoruz. Bu yapılandırma, 53. satırdaki [métier] ve 56. satırdaki [admindata] küresel değişkenlerini tanımlamaktan ibarettir; bu değişkenler daha sonra örneğin 12. satırdaki [TestDaoMétier] yöntemi tarafından kullanılacaktır;
- satır 39-47: Test komut dosyası, [mysql / pgres] parametresini bekler; bu parametre, MySQL veya PostgreSQL veritabanının kullanılıp kullanılmadığını belirtir;
- 50-51. satırlar: test yapılandırılır;
-
- satır: Yapılandırmadan [métier] katmanı alınır;
- satır 56: Aynı işlem [dao] katmanı için de yapılır. Ardından, vergi hesaplaması için gerekli verileri içeren [admindata] örneği alınır;
- testler, 66. satırdaki [unittest.main()] yönteminin, komut dosyası tarafından alınan [mysql / pgres] parametresini yok saymadığını, ancak ona farklı bir anlam yüklediğini göstermiştir. 63. satır, bu yöntemin artık hiçbir parametreye sahip olmamasını sağlar;
İki farklı yürütme yapılandırması oluşturuyoruz:


Bu iki yapılandırmadan birini çalıştırdığımızda şu sonuçları elde ediyoruz:
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
-
- ve 7. satırlar: 11 testin tümü başarılı oldu;
Bu testlerin yalnızca 11 vergi hesaplama durumunu kontrol ettiğini hatırlayalım. Yine de bu testlerin başarılı olması, [dao] katmanına güvenmemiz için yeterli olabilir.
20.2.5. Ana komut dosyası


Ana komut dosyası [main], sürüm 4'tekiyle aynıdır:
# mysql veya pgres parametresi bekleniyor
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()
# uygulama yapılandırılıyor
import config
config = config.configure({'sgbd': sgbd})
# syspath belirlenir - içe aktarmalar yapılabilir
from ImpôtsError import ImpôtsError
# uygulamanın katmanları alınır (zaten örneklenmişlerdir)
dao = config["dao"]
métier = config["métier"]
try:
# vergi dilimlerinin alınması
admindata = dao.get_admindata()
# Vergi mükelleflerinin verileri okunuyor
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# Vergi mükellefleri mi?
if not taxpayers:
raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# Vergi mükelleflerinin vergisinin hesaplanması
for taxPayer in taxpayers:
# taxPayer hem giriş hem de çıkış parametresidir
# taxPayer değiştirilecek
métier.calculate_tax(taxPayer, admindata)
# sonuçların bir metin dosyasına yazılması
dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
# hatanın görüntülenmesi
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# işlem tamamlandı
print("Travail terminé...")
Notlar
- 1-10. satırlar: Kullanılacak SGBD değerini belirten [mysql / pgres] parametresi alınır;
- 12-14. satırlar: uygulama yapılandırılır;
- 16-17. satırlar: [ImpôtsError] sınıfı içe aktarılır. Bu sınıfa 38. satırda ihtiyaç duyulur;
- 19-21. satırlar: Uygulamanın katmanlarına ilişkin referanslar alınır;
-
- satır: [dao] katmanından vergi idaresi verileri istenir. [métier] katmanı, vergi hesaplaması için bu verilere ihtiyaç duyar;
-
- satır: vergi mükelleflerinin verilerini (kimlik numarası, evli olup olmadığı, çocuk sayısı, maaş) bir listeye alıyoruz;
- 29-30. satırlar: Bu liste boşsa, bir istisna oluşturulur;
- satır 32-35: [taxpayers] listesindeki öğelerin vergisi hesaplanır;
- satır 37: sonuçların jSON[résultats.json] dosyasına yazılması;
- satır 38-40: Olası hataların yönetimi;
Komut dosyasının çalıştırılması için iki adet |configurations d’exécution| dosyası oluşturulur:

[résultats.json] dosyasında elde edilen sonuçlar, sürüm 4'e aittir.

20.3. Uygulama 3: Etkileşimli modda vergi hesaplaması
Şimdi, vergiyi etkileşimli bir şekilde hesaplamaya olanak tanıyan uygulamayı tanıtacağız. Bu, sürüm 4'teki Uygulama 2'nin uyarlanmış halidir.


- [main] komut dosyası, [ui] katmanındaki [ui.run] yöntemi ile kullanıcıyla diyalog başlatır;
- [ui] katmanı:
- vergi hesaplaması için gerekli verileri elde etmek üzere [dao] katmanını kullanır;
- kullanıcıdan, vergisi hesaplanacak vergi mükellefi ile ilgili bilgileri ister;
- bu hesaplamayı yapmak için [métier] katmanını kullanır;
[config_layers] dosyası ek bir katmanı oluşturur:
def configure(config: dict) -> dict:
# DAO katmanının örneklenmesi
from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)
# katmanın örneklenmesi [métier]
from ImpôtsMétier import ImpôtsMétier
config['métier'] = ImpôtsMétier()
# kullanıcı arayüzü
from ImpôtsConsole import ImpôtsConsole
config['ui'] = ImpôtsConsole(config)
# yapılandırma döndürülüyor
return config
[ImpôtsConsole] sınıfının 11-12. satırları, |version 4|'teki ile aynıdır.
Ana komut dosyası [main] şu şekildedir:
# mysql veya pgres parametresi bekleniyor
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()
# uygulamayı yapılandırıyoruz
import config
config = config.configure({'sgbd': sgbd})
# syspath yapılandırıldı - içe aktarmalar yapılabilir
from ImpôtsError import ImpôtsError
# Q katmanı alınıyor [ui]
ui = config["ui"]
# kod
try:
# [ui] katmanının çalıştırılması
ui.run()
except ImpôtsError as ex1:
# hata mesajı görüntülenir
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# hata mesajı görüntülenir
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# her durumda yürütülür
print("Travail terminé...")
- 1-10. satırlar: komut dosyası, kullanılacak SGBD'i belirten bir [mysql / pgres] parametresini bekler;
- 12-14. satırlar: uygulama yapılandırılır;
- 19-20. satırlar: yapılandırmadan [ui] katmanı alınır;
-
- satır: Katman çalıştırılır;
Sonuçlar, |version 4| ile aynıdır. Sürüm 4'teki tüm arayüzler sürüm 5'te de korunmuş olduğundan, başka türlü olması mümkün değildi.