Skip to content

23. Uygulama alıştırması: sürüm 6

23.1. Giriş

Şimdi vergi hesaplama uygulamamıza geri dönüyoruz. Bu uygulama etrafında çeşitli web uygulamaları geliştireceğiz.

Uygulama alıştırmamızın 5. sürümünde, vergi idaresine ait veriler bir veritabanında düzenlenmişti. Bu 5. sürüm, birbirinden ayrı ancak ortak katmanlara sahip iki uygulama içeriyordu:

  • bir metin dosyasında kayıtlı vergi mükellefleri için |batch| modunda vergi hesaplayan bir uygulama;
  • bilgileri klavyeyle girilen vergi mükellefleri için |interactif| modunda vergi hesaplayan bir uygulama;

Toplu iş modunda (batch) vergi hesaplama uygulamasının 5. sürümü şu mimariye sahipti:

Image

Sonunda, bu uygulamanın web sürümü aşağıdaki mimariye sahip olacaktır:

Image

  • [1] web istemcisi, [2] web sunucusuna bağlanır; bu sunucu da SGBD ve [3] ile iletişim kurar;
  • [2] web sunucusu, orijinal uygulamanın [métier], [8] ve [dao], [9] katmanlarını korur;
  • orijinal uygulama, ana komut dosyası [4] ile [métier] ve [15] katmanlarını korur. [métier], [8] ve [15] katmanları birbirinin aynısıdır;
  • istemci/sunucu iletişimi için iki ek katman gereklidir:
    • web uygulamasını uygulayan [web] ve [7] katmanları;
    • [7] web uygulamasının istemcisi olan [dao] [5] katmanı;

Nihai sürümde, toplu vergi hesaplaması iki şekilde gerçekleştirilebilecektir:

  • vergi hesaplaması, sunucudaki [métier] katmanı tarafından gerçekleştirilir. [main] komut dosyası bu yöntemi kullanacaktır;
  • Vergi hesaplaması, istemcinin [métier] katmanı tarafından gerçekleştirilir. [main2] komut dosyası bu yöntemi kullanacaktır;

Bundan sonra, yukarıdaki türde birkaç istemci/sunucu uygulaması geliştireceğiz; her biri bir veya birkaç yeni web geliştirme teknolojisini örnekleyecektir.

23.2. Vergi hesaplama web sunucusu

23.2.1. Sürüm 1

Image

[server_01] komut dosyası, aşağıdaki web uygulamasıdır:

Image

  • [1]'te, üç değerin aktarıldığı, önceden ayarlanmış bir URL kullanılır:
    • [marié] (evet / hayır), vergi mükellefinin evli olup olmadığını belirtmek için;
    • [enfants]: vergi mükellefinin çocuk sayısı;
    • [salaire]: vergi mükellefinin yıllık maaşı;
  • [2]'te, web sunucusu, ödenecek vergi tutarını ve bunun çeşitli bileşenlerini gösteren jSON dizesini döndürür;

Uygulamanın mimarisi şu şekildedir:

Image

  • [1] tarayıcısı, [2] sunucusuna sorgu gönderir. [server_01] komut dosyası, sunucunun [web] [2] katmanını uygular;
  • [3-8] katmanları, vergi hesaplama uygulamasındaki |version 5|'te halihazırda kullanılan katmanlardır. Bunları olduğu gibi devralıyoruz;
    • [métier] ve [3] katmanları, |ici| olarak tanımlanmıştır;
    • [dao] ve [4] katmanları, |ici| olarak tanımlanmıştır;

[server_01] web uygulaması, uygulamanın tamamını yapılandıran üç komut dosyası kullanılarak yapılandırılmıştır:

  • Uygulamanın tamamını yapılandıran [config];
  • Veritabanına erişimi yapılandıran [config_database]. SGBD, MySQL ve PostgreSQL ile çalışacağız;
  • Uygulamanın katmanlarını yapılandıran [config_layers];

[config] komut dosyası şu şekildedir:


def configure(config: dict) -> dict:
    import os

    # 1. adım ------
    # bu dosyanın bulunduğu klasör
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # kök dizin
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # mutlak bağımlılıklar
    absolute_dependencies = [
        # proje klasörleri
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # 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",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Sabitler, aralıklar
        f"{root_dir}/impots/v05/entities",
        # IndexController
        f"{script_dir}/../controllers",
        # komut dosyaları [config_database, config_layers]
        script_dir,
    ]
    # syspath ayarlanıyor
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # 2. adım ------
    # uygulamanın yapılandırılması
    # uygulamayı kullanmaya yetkili kullanıcıların listesi
    config['users'] = [
        {
            "login""admin",
            "password""admin"
        }
    ]

    # 3. adım ------
    # veritabanı yapılandırması
    import config_database
    config["database"] = config_database.configure(config)

    # 4. adım ------
    # uygulama katmanlarının örneklenmesi
    import config_layers
    config['layers'] = config_layers.configure(config)

    # yapılandırmayı geri yükleme
    return config
  • [configure] işlevi, bir [config] sözlüğünü parametre olarak alır (1. satır) ve içeriğini zenginleştirdikten sonra bunu sonuç olarak döndürür (54. satır). [config] sonucunu döndürmenin gereksiz olduğu çoktan söylenebilirdi. Zira [config], çağıran kod ile çağrılan kod arasında paylaşılan bir sözlük referansıdır. Dolayısıyla çağıran kod bu referansa zaten sahiptir (satır 1) ve ona tekrar vermek gereksizdir (satır 54). Bu nedenle şöyle yazmak gerekir:

config=[module].configure(config) (1)

yazmak gereksizdir. Şöyle yazmak yeterlidir:


[module].configure(config) (2)

Bununla birlikte, (1) numaralı yazım biçimini korudum çünkü bunun, çağrılan kodun [config] sözlüğünü değiştirdiğini belki de daha iyi gösterdiğini düşündüm.

  • 1. satır: [configure] işlevi tarafından alınan [config] sözlüğünde, değerini [‘mysql’, ‘pgres’] listesinden alan ‘sgbd’ anahtarı bulunmaktadır. [mysql], kullanılan veritabanının MySQL tarafından yönetildiğini gösterirken, ‘pgres’ ise kullanılan veritabanının PostgreSQL tarafından yönetildiğini gösterir;
  • 4-27. satırlar: Web uygulaması için gerekli öğeleri içeren tüm klasörler listelenir. Bunlar, uygulamanın Python Yolu’nun bir parçası olacaktır (30-31. satırlar);
  • satır 33-40: Uygulamaya yalnızca belirli kullanıcıların erişmesine izin verilecektir. Burada tek bir kullanıcıdan oluşan bir liste bulunmaktadır;
  • satır 43-46: Kullanılan veritabanı yapılandırmasını oluşturan [config_database] komut dosyasıdır;
  • 46. satır: [config_database] betiği tarafından oluşturulan yapılandırma, ‘database’ anahtarıyla ilişkilendirilmiş genel yapılandırmaya eklenen bir sözlüktür;
  • satır 48-51: [config_layers] komut dosyası, web uygulamasının katmanlarını başlatır. Bu komut dosyası, ‘layers’ anahtarıyla ilişkili genel yapılandırmaya kaydedilen bir sözlük döndürür;

[config_database] betiği, |version 5|'te zaten kullanılmış olan betiktir. Hatırlatmak amacıyla tekrar veriyoruz:


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ı bilgileri kaydedip bir sözlükte geri döndürür
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

[config_layers] komut dosyası, web sunucusunun katmanlarını yapılandırır. Daha önce karşılaştığımız |script| komut dosyasını tekrar kullanıyoruz:


def configure(config: dict) -> dict:
    # uygulama katmanlarının örneklenmesi
    
    # DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)
    
    # iş mantığı
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # katman örneklerini bir sözlüğe yerleştirip çağıran koda döndürülür
    return {
        "dao": dao,
        "métier": métier
    }
  • 6. satır: [dao] katmanı bir veritabanı ile uygulanmıştır;
  • [ImpotsDaoWithAdminDataInDatabase], |ici| olarak tanımlanmıştır;
  • [ImpôtsMétier], |ici| olarak tanımlanmıştır;

Ana komut dosyası [server_01] şu şekildedir:


# mysql veya pgres parametresi beklenir
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})

# bağımlılıklar
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status

# Vergi idaresinden verilerin alınması
try:
    # admindata, uygulama kapsamında salt okunur bir veri olacaktır
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# Flask uygulaması
app = Flask(__name__)


# Ana Sayfa URL : /?evli=xx&çocuk=yy&maaş=zz
@app.route('/', methods=['GET'])
def index():
    # başlangıçta hata yok
    erreurs = []
    # istekte URL adresine üç parametre gönderilmelidir
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # medeni durum bilgisi URL'ten alınır
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    # çocuk sayısı URL'ten alınır
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants [{enfants}] invalide")
        else:
            enfants = int(enfants)

    # URL'ten maaş alınır
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire [{salaire}] invalide")
        else:
            salaire = int(salaire)

    # URL'te geçersiz parametreler mi var?
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # Hatalar var mı?
    if erreurs:
        # müşteriye bir hata yanıtı gönderilir
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # hata yok, çalışmaya devam edilebilir
    # vergi hesaplaması
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # yanıt müşteriye gönderiliyor
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


# sadece ana işlem
if __name__ == '__main__':
    # Flask sunucusunu başlatıyoruz
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • 1-10. satırlar: Hangi SGBD'in kullanılacağını belirten parametre alınır;
  • 12-14. satırlar: Bu bilgiyle uygulamayı yapılandırabiliriz. Özellikle Python Path oluşturulur;
  • 16-23. satırlar: Yeni Python Yolu kullanılarak ihtiyaç duyulan öğeler içe aktarılır;
  • 25-31. satırlar: Vergi hesaplamasını yapmak için gerekli vergi idaresi verileri alınır;
  • satır 33-34: Flask uygulamasının örneği oluşturulur;
  • 38. satır: Flask uygulaması yalnızca URL [/]'i işler. Uygulama, aşağıdaki şekilde yapılandırılmış bir URL bekler: [/ ?marié=xx&enfants=yy&salaire=zz] ile:
    • xx: evet / hayır;
    • yy: çocuk sayısı;
    • zz: yıllık maaş;
  • satır 40-89: URL parametrelerinin geçerliliği kontrol edilir;
  • satır 41: hata mesajları [erreurs] listesinde toplanır;
  • satır 43: ayarlanan URL parametrelerinin [request.args]'te bulunduğu unutulmamalıdır (bkz. |ici|):
    • [request] nesnesi, 20. satırda içe aktarılan Flask nesnesidir;
    • [request.args] nesnesi bir sözlük gibi davranır;
  • 43-44. satırlar: tam olarak üç parametre olup olmadığı kontrol edilir (ne daha az, ne daha fazla);
  • 46-49. satırlar: [marié] parametresinin URL içinde mevcut olup olmadığı kontrol edilir;
  • 50-54. satırlar: Eğer varsa, başındaki ve sonundaki ‘boşluklar’dan arındırılmış küçük harfli değerinin “evet” mi yoksa “hayır” mı olduğu kontrol edilir;
  • satır 56-59: [enfants] parametresinin URL içinde olup olmadığı kontrol edilir;
  • satır 60-66: Varsa, değerinin pozitif bir tamsayı olup olmadığı kontrol edilir;
  • 66. satır: URL parametrelerinin ve değerlerinin karakter dizileri olduğu unutulmamalıdır. [enfants] parametresinin değeri ‘int’ türüne dönüştürülür;
  • satır 68-78: [salaire] parametresi için, [enfants] parametresinde olduğu gibi aynı testler yapılır;
  • satır 81-83: URL içinde [‘marié, ‘enfants’, ‘salaire’] dışında başka parametre bulunmadığını kontrol ederiz;
  • 85-89. satırlar: Tüm bu kontrollerden sonra [erreurs] listesi boş değilse, bu hata listesini bir jSON dizesi ve [400 Bad Request] durum kodu şeklinde müşteriye gönderilir;

Daha sonra, müşteriye yanıt olarak sık sık bir jSON dizesi gönderme fırsatımız olacağından, bu gönderim için gerekli birkaç satır, daha önce kullandığımız [myutils.py] modülüne taşınmıştır:

Image

[myutils.py] komut dosyası şu şekilde olur:


# ithalatlar
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    # absolute_dependencies: mutlak klasör adlarının listesi

    ….


# yanıtın oluşturulması HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    # HTTP yanıtının gövdesi
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    # yanıt gövdesi HTTP, jSON'ten gelir
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    # HTTP yanıtı gönderilir
    return response, status_code
  • 16. satır: [json_response] işlevi iki parametre bekler:
    • [réponse]: jSON dizesini web istemcisine göndermek için gerekli sözlük;
    • [status_code]: yanıtın durum kodu HTTP;
  • 18. satır: yanıtın gövdesi jSON belirlenir;
  • 20. satır: Web istemcisine jSON alacağını bildiren HTTP başlığı eklenir;
  • 22. satır: HTTP yanıtı çağıran koda gönderilir. Web istemcisine gönderme görevi ona aittir;

[__init__.py] dosyası şu şekilde değişir:


from .myutils import set_syspath, json_response

[myutils] dosyasının yeni sürümü, bir Pycharm terminalinde [pip install .] komutu ile makine kapsamındaki modüller arasına yüklenir:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
  Attempting uninstall: myutils
    Found existing installation: myutils 0.1
    Uninstalling myutils-0.1:
      Successfully uninstalled myutils-0.1
    Running setup.py install for myutils ... done
Successfully installed myutils-0.1
  • 1. satır: Bu komutu girmek için [packages] klasöründe olmanız gerekir;

[server_01] komut dosyasının kodu şu şekilde devam ediyor:



    # Hata mı var?
    if erreurs:
        # müşteriye bir hata yanıtı gönderilir
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # hata yok, çalışmaya devam edilebilir
    # vergi hesaplaması
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # yanıt müşteriye gönderiliyor
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)

  • 10. satır: bu noktaya geldiğimizde, URL'te beklenen parametreler mevcut ve doğrudur;
  • 10. satır: vergi mükellefini modelleyen [TaxPayer] nesnesi oluşturulur;
  • 11. satır: [métier] katmanından vergiyi hesaplaması istenir. [métier] katmanı tarafından hesaplanan öğelerin, parametre olarak geçirilen [taxpayer] nesnesine eklendiği hatırlatılmalıdır;
  • 13. satır: Yanıt, bir jSON dizesi biçiminde web istemcisine gönderilir. Bu, bir sözlüğün jSON dizesidir. [result] anahtarıyla ilişkilendirilerek, buraya [taxpayer] nesnesinin sözlüğü yerleştirilir. [taxpayer] nesnesinin kendisi, jSON formatında serileştirilemediği için buraya eklenemedi;

İki yürütme yapılandırması oluşturulur; biri MySQL için, diğeri PostgreSQL için:

Image

İşte birkaç yürütme örneği ([server_01] uygulamasını ve kullanılan SGBD'i başlattınız, ardından bir tarayıcıyla URL http://localhost:5000/ adresini çağırdınız):

Image

Image

İşte Postman konsolunda bir yürütme örneği:

Image


GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT

{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
  • 1. satır: hatalı bir URL isteniyor;
  • 10. satır: sunucu 400 durum koduyla yanıt veriyor BAD REQUEST;

23.2.2. Sürüm 2

Image

Sunucunun sürüm 2'si, URL işlemeyi [index_controller] [5] modülüne ayırır:


# bağımlılıkların içe aktarılması
import re

from flask_api import status
from werkzeug.local import LocalProxy


# URL parametreli: /?evli=xx&çocuk sayısı=yy&maaş=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # bağımlılıklar
    from TaxPayer import TaxPayer

    # başlangıçta hata yok
    erreurs = []
    # istekte üç parametre bulunmalıdır
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # URL'in medeni durumunu alıyoruz
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    # URL'in çocuk sayısı alınır
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants {enfants} invalide")
        else:
            enfants = int(enfants)

    # URL'in maaşı alınır
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire {salaire} invalide")
        else:
            salaire = int(salaire)

    # URL'te başka parametreler var mı?
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # Hata mı var?
    if erreurs:
        # müşteriye bir hata yanıtı gönderilir
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # hata yok, çalışmaya devam edilebilir
    # vergi hesaplaması
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    # yanıt müşteriye gönderiliyor
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • 9. satır: [execute] işlevi iki parametre alır:
    • [request]: istemciden gelen HTTP isteği;
    • [config]: uygulamanın yapılandırma sözlüğü;

[server_02] komut dosyası şöyledir:


# 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})

# bağımlılıklar
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

# Vergi idaresinden verilerin alınması
try:
    # admindata, uygulama kapsamında salt okunur bir veri olacaktır
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# Flask uygulaması
app = Flask(__name__)


# Ana Sayfa URL : /?evli=xx&çocuk=yy&maaş=zz
@app.route('/', methods=['GET'])
def index():
    # istek yürütülür
    résultat, statusCode = index_controller.execute(request, config)
    # yanıt gönderiliyor
    return json_response(résultat, statusCode)


# sadece ana iş parçacığı
if __name__ == '__main__':
    # sunucu başlatılıyor
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • 36-41. satırlar: / yolunun işlenmesi;
  • 39. satır: [IndexController.execute] işlevinin kullanımı;

Artık şu tekniği kullanacağız: her rota, kendisine özgü bir modül tarafından işlenecektir.

Çalıştırma sonuçları, sürüm 1 ile aynıdır.

23.2.3. Sürüm 3

Image

Sürüm 3, kimlik doğrulama kavramını getirir.

[server_03] komut dosyası şu şekilde olur:


# 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})

# bağımlılıklar
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller

# vergi idaresinden veri alınması
try:
    # config[‘admindata’], uygulama kapsamında salt okunur bir veri olacaktır
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# kimlik doğrulama yöneticisi
auth = HTTPBasicAuth()


# kimlik doğrulama yöntemi
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    # kullanıcı listesi
    users = config['users']
    # bu liste taranır
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # bulunamadı
    return False


# Flask uygulaması
app = Flask(__name__)


# Ana Sayfa URL : /?evli=xx&çocuk=yy&maaş=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    # isteği çalıştırıyoruz
    résultat, statusCode = index_controller.execute(request, config)
    # yanıt gönderiliyor
    return json_response(résultat, statusCode)


# sadece ana iş parçacığı
if __name__ == '__main__':
    # sunucu başlatılıyor
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • 21. satır: Bir kimlik doğrulama yöneticisi içe aktarılır. Bir web sunucusunda çeşitli kimlik doğrulama türleri vardır. Burada kullandığımızın adı [HTTP Basic]'tir. Her kimlik doğrulama türü, belirli bir istemci/sunucu diyaloğunu izler;
  • 33. satır: Kimlik doğrulama yöneticisinin bir örneği oluşturulur;
  • 37. satır: [@auth.verify_password] anotasyonu, kimlik doğrulama yöneticisi, [HTTP Basic] protokolüne göre istemci tarafından iletilen kullanıcı adı ve şifreyi doğrulamak istediğinde çalıştırılacak işlevi işaretler;
  • 55. satır: [@auth.login_required] etiketinin, web istemcisinin kimlik doğrulamasının yapılması gereken bir rotayı işaretlediği görülür. Web istemcisi henüz kimlik bilgilerini göndermediyse, web sunucusu HTTP basic protokolüne göre otomatik olarak bu bilgileri isteyecektir;

[flask_httpauth] modülünün yüklenmiş olması gerekir:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
  Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.0.1)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0

Postman konsolunda neler olduğunu görelim. Siz:

  • bir çalıştırma yapılandırması oluşturun;
  • web uygulamasını başlatın;
  • istediğiniz SGBD'i başlatın;
  • Postman ile URL [/]'i çağırın;

Postman konsolundaki istemci/sunucu diyaloğu şu şekildedir:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:05:37 GMT

Unauthorized Access
  • 10. satır: sunucu, URL ve [/]'e erişim iznimiz olmadığını bildiriyor;
  • 13. satır: Kullanmamız gereken kimlik doğrulama protokolünü belirtir; burada "Temel Kimlik Doğrulama" olarak adlandırılan protokol;

Postman’ı, kullanıcı kimlik bilgilerini Auth Basic protokolüne göre gönderecek şekilde yapılandırmak mümkündür:

Image

  • [6-7] kısmına, [config] komut dosyasında bulunan kimlik bilgilerini giriyoruz: Image

    config['users'] = [
        {
            "login": "admin",
            "password": "admin"
        }
    ]

Postman konsolundaki istemci/sunucu diyaloğu şu şekilde olur:


GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
  • 2. satır: Postman istemcisi, [admin / admin] kullanıcısının kimlik bilgilerini şifreli olarak gönderir;
  • 17. satır: Sunucu doğru şekilde yanıt veriyor. [marié, enfants, salaire] (1. satır) parametreleri gönderilmediği için hata bildiriyor, ancak kimlik doğrulama hatası bildirmiyor;

Şimdi bir tarayıcıyla (aşağıda Firefox) URL / adresini isteyelim:

Image

  • Postman'da olduğu gibi, Firefox da sunucudan HTTP yanıtını, HTTP başlıklarıyla birlikte aldı:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

Firefox, diğer tarayıcılar gibi bu başlıkları aldığında diyalogu durdurmaz. Kullanıcıdan sunucunun talep ettiği kimlik bilgilerini ister. Yukarıda admin / admin yazmak, sunucudan yanıt almak için yeterlidir:

Image

23.3. Vergi hesaplama sunucusunun web istemcisi

23.3.1. Giriş

Önceki paragrafta, vergi hesaplama sunucusunun web istemcisi bir tarayıcıydı. Bu bölümde ise web istemcisi bir konsol komut dosyası olacaktır. Mimari şu şekilde olacaktır:

Image

  • web istemcisi [1-2] katmanlarından oluşur;
  • web sunucusu ise [3-9] katmanlarından oluşur. Bir önceki paragrafta belirtildiği gibi;

Dolayısıyla [1-2] katmanlarını yazmamız gerekiyor.

[dao] ve [2] katmanları, [3] web sunucusuyla iletişim kurabilmelidir. Artık HTTP protokolünü biliyoruz ve örneğin daha önce incelediğimiz [pycurl] modülüyle, [3] web sunucusuyla iletişim kuran bir komut dosyası yazabiliriz. Ancak, HTTP istemci/sunucu iletişimlerinde uzmanlaşmış modüller de mevcuttur. Bunlardan biri olan [requests] modülünü kullanacağız:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
  Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
     || 61 kB 137 kB/s
Collecting idna<3,>=2.5
  Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
     || 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
  Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
     || 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
  Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
     || 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
  Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
     || 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9

Web istemcisinin komut dosyası yapısı şu şekildedir:

Image

Komut dosyası, |version 1|'te açıklanan toplu iş modunda vergi hesaplama uygulamasını hayata geçirecektir. Bu uygulamanın en son sürümü |version 5|'tir. Çalışma şekli şöyle özetlenebilir:

  • Vergisi hesaplanacak vergi mükellefleri, [taxpayersdata.txt] adlı metin dosyasında toplanır:
# geçerli veriler: kimlik numarası, evli, çocuk sayısı, maaş
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# hatalı satırlar oluşturuluyor
# yeterli değer yok
11,12
# hatalı değerler
x,x,x,x
  • sonuçlar iki dosyaya kaydedilir:
  • [errors.txt] metin dosyası, vergi mükellefleri dosyasında tespit edilen hataları toplar:

Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt

Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
  • (devamı)
    • jSON [résultats.json] dosyası, farklı vergi mükelleflerinin vergi hesaplama sonuçlarını bir araya getirir:

[
  {
    "id": 0,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },

]

23.3.2. Web istemcisinin yapılandırılması

Image

Yapılandırma, iki komut dosyası kullanılarak gerçekleştirilir:

  • [config], mimarinin katmanları dışındaki tüm yapılandırmayı sağlar;
  • [config_layers], mimarinin katmanlarının yapılandırılmasını sağlar;

[config] komut dosyası şöyledir:


def configure(config: dict) -> dict:
    import os

    # 1. adım ------

    # bu dosyanın klasörü
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # kök dizin
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # mutlak bağımlılıklar
    absolute_dependencies = [
        # proje klasörleri
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # 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",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Sabitler, dilimler
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # yapılandırma komut dosyaları
        script_dir,
    ]

    # syspath'ı ayarlıyoruz
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # 2. adım ------
    # sabit değerlerle uygulama yapılandırması
    config.update({
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        "server": {
            "urlServer""http://127.0.0.1:5000/",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            }
        }
    }
    )

    # 3. adım ------
    # katmanların örneklenmesi
    import config_layers
    config['layers'] = config_layers.configure(config)

    # yapılandırmayı uyguluyoruz
    return config
  • 1. satır: [configure] işlevi, yapılandırma bilgileriyle doldurulacak sözlüğü parametre olarak alır. Bu sözlük önceden doldurulmuş veya boş olabilir. Burada boş olacaktır;
  • satır 40-42: [dao] katmanı tarafından yönetilen üç metin dosyasının mutlak adları;
  • satır 43-50: [server] anahtarıyla ilişkili olarak, [dao] katmanının iletişim kurması gereken web sunucusu hakkında bilmesi gereken bilgiler:
    • satır 44: web hizmetinin URL anahtarı;
    • satır 45: [authBasic] anahtarı, URL'e erişim için Basic türünde kimlik doğrulama gerekiyorsa True değerindedir;
    • satır 46-49: kimlik doğrulama istenirse, kimlik doğrulaması yapacak kullanıcının kimlik bilgileri;
  • 56-57. satırlar: Katmanlar oluşturulur; burada tek katman olan [dao] oluşturulur ve katman referansları, [layers] anahtarıyla ilişkili [config] içine yerleştirilir;

[config_layers] komut dosyası şu şekildedir:


def configure(config: dict) -> dict:
    # uygulama katmanlarının örneklenmesi

    # DAO katmanı
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # katman yapılandırması
    return {
        "dao": dao
    }
  • 1. satır: [configure] işlevi, uygulamayı yapılandıran sözlüğü alır;
  • 4-6. satırlar: [dao] katmanı örneklenir. 6. satırda, katmana uygulamanın yapılandırması aktarılır; katman, ihtiyaç duyduğu bilgileri bu yapılandırmada bulur;
  • satır 8-11: [dao] katmanının referansını içeren bir sözlük döndürülür;

23.3.3. Ana komut dosyası [main]

Ana komut dosyası [main], |version 5| komut dosyasının bir varyantıdır:


# uygulamayı yapılandırıyoruz
import config
config = config.configure({})

# bağımlılıklar
from ImpôtsError import ImpôtsError

# kod
try:
    # katmanı alıyoruz [dao]
    dao = config["layers"]["dao"]
    # vergi mükelleflerinin verilerinin okunması
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # vergi mükelleflerinin?
    if not taxpayers:
        raise ImpôtsError(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
        dao.calculate_tax(taxpayer)
    # 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é...")
  • 2-3. satırlar: uygulama yapılandırılmıştır;
  • 13. satır: [dao] katmanı, vergisi hesaplanacak vergi mükelleflerinin listesini sağlar;
  • 21. satır: [dao] katmanı, her birinin vergisini hesaplar;
  • satır 23: sonuçlar jSON dosyasına kaydedilir;

23.3.4. [dao] katmanının uygulanması

Image

Kullanılan istemci/sunucu mimarisine geri dönelim:

Image

  • [2, 6]'te, [dao] katmanının iki rolü olduğu görülmektedir:
    • hem vergi mükelleflerinin verilerini okumak hem de vergi hesaplama sonuçlarını yazmak için dosya sistemine erişir. Bunu yapabilen bir |AbstractImpôtsDao| sınıfımız zaten mevcut. Bu sınıf, |version 4|'ten itibaren kullanılmaktadır;
    • [3] web sunucusuyla iletişim kurar;

|version 5|'te, ana komut dosyası [main] [1], [métier] [4] katmanıyla doğrudan iletişim kuruyordu. Bu komut dosyasını değiştirmek istemiyoruz. Bunun için, [dao] ve [2] katmanlarının, [métier] ve [4] katmanlarının arayüzünü uygulamasını sağlayacağız. Böylece ana komut dosyası [main], [métier] ve [4] katmanlarıyla doğrudan iletişim kuruyormuş gibi algılayacak ve bu katmanların başka bir makinede bulunduğunu tamamen göz ardı edebilecektir.

[dao] [2] katmanını uygulayan sınıf tanımı şu şekilde olabilir:


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • [ImpôtsDaoWithHttpClient] sınıfı:
    • [AbstractImpôtsDao] sınıfından miras alır; bu sayede [6] dosya sistemi ile iletişimi yönetebilir;
    • [InterfaceImpôtsMétier] arayüzünü uygular; böylece |version 5|'in ana betiği olan [main]'i değiştirmek zorunda kalmaz;

[ImpôtsDaoWithHttpClient] sınıfının tam kodu şöyledir:


# ithalatlar
import requests
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # oluşturucu
    def __init__(self, config: dict):
        # üst sınıfın başlatılması
        AbstractImpôtsDao.__init__(self, config)
        # parametrelerin kaydedilmesi
        self.__config_server = config["server"]

    # [AbstractImpôtsDao]'in kullanılmayan yöntemi
    def get_admindata(self) -> AdminData:
        pass

    # vergi hesaplaması
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        # istisnaların yukarıya aktarılmasına izin verilir
        # get parametreleri
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # Auth Basic kimlik doğrulamalı bağlantı?
        if self.__config_server['authBasic']:
            response = requests.get(
                # sorgulanan sunucunun URL'i
                self.__config_server['urlServer'],
                # URL parametreleri
                params=params,
                # Basic kimlik doğrulaması
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            # Auth Basic kimlik doğrulaması olmadan bağlantı
            response = requests.get(self.__config_server['urlServer'], params=params)
        # doğrulama
        print(response.text)
        # HTTP yanıtının durum kodu
        status_code = response.status_code
        # jSON yanıtını bir sözlüğe eklenir
        résultat = response.json()
        # durum kodu 200'den farklıysa hata OK
        if status_code != status.HTTP_200_OK:
            # hataların yanıtın [erreurs] anahtarıyla ilişkilendirildiği biliniyor
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # sonucun yanıtın [result] anahtarıyla ilişkilendirildiği biliniyor
        # giriş parametresi bu sonuçla değiştiriliyor
        taxpayer.fromdict(résultat["réponse"]["result"])
  • 21-23. satırlar: [AbstractImpôtsDao] sınıfı (12. satır), [get_admindata] adlı bir soyut yönteme sahiptir. Bu yöntemi kullanmasak bile (admindata, istemci tarafından değil sunucu tarafından yönetilir) onu uygulamak zorundayız;
  • 26. satır: [calculate_tax] yöntemi, [InterfaceImpôtsMétier] arayüzüne (12. satır) aittir. Bunu uygulamamız gerekir;
  • 15. satır: Oluşturucu, tek parametre olarak uygulama yapılandırma sözlüğünü alır;
  • satır 16-17: üst sınıf [AbstractImpôtsDao], yine uygulama yapılandırması aktarılarak başlatılır. Burada, yönetmesi gereken üç metin dosyasının adlarını bulacaktır;
  • 18-19. satırlar: Vergi hesaplama web sunucusuna ait bilgiler sınıf içinde yerel olarak saklanır;
  • 26. satır: [calculate_tax] yöntemi, parametre olarak |Taxpayer| türünde bir nesne alır. [InterfaceImpôtsMétier.calculate_tax] yönteminin imzasına uymak için, vergi idaresinin verilerini kapsadığı varsayılan bir [admindata] parametresi de alır. İstemci tarafında bu veriler mevcut değildir. Bu parametre her zaman [None] olarak kalacaktır. Bu çelişki, [ImpôtsMétier] sınıfının başlangıçta yanlış yazıldığını göstermektedir:
  • [calculate_tax]'in imzası basitçe şöyle olmalıydı:

def calculate_tax(self, taxpayer: TaxPayer)

ve [admindata : AdminData] parametresi sınıfın oluşturucusuna aktarılmalıydı;

  • 27. satır: [calculate_tax] yönteminin kodu bir try / catch / finally bloğu içine alınmamıştır. Bu, olası istisnaların yönetilmeyeceği ve çağıran koda, yani [main] betiğine iletileceği anlamına gelir. Bu komut dosyası, [dao] katmanından gelen tüm istisnaları başarıyla durdurur;
  • 28. satır: Vergi hesaplaması sunucu tarafında yapılır. Dolayısıyla sunucuyla iletişim kurmak gerekecektir. Bu, 2. satırda içe aktarılan [requests] modülü ile gerçekleştirilir;
  • satır 31-43: Web sunucusuna bir GET isteği göndermek için [requests.get] yöntemi kullanılır:
    • 33-34. satırlar: Yöntemin ilk parametresi, iletişim kurulacak URL'tir;
    • satır 35-40: Diğer iki parametre, sırası önemli olmayan adlandırılmış parametrelerdir;
    • 35-36. satırlar: [params] adlı parametrenin değeri, URL'e [/url ?param1=valeur1&param2=valeur2&…] biçiminde eklenecek bilgileri içeren bir sözlük olmalıdır;
    • 29. satır: Web sunucusunun beklediği üç parametreyi ([marié, enfants, salaire]) içeren sözlük. Bu parametrelerin geçmesi gereken kodlama (urlencoded olarak adlandırılır) ile ilgilenmemize gerek yoktur. [requests] bunu halleder;
    • 37-40. satırlar: [auth] adlı parametre, iki elemandan (login, password) oluşan bir tuple'dır. Bu, Basic türünde bir kimlik doğrulama işleminin kimlik bilgilerini temsil eder;
  • 44-45. satırlar: Bu iki satırın tek amacı eğitim amaçlıdır (hata ayıklama tamamlandığında yorum satırına alınacaktır):
    • [response], sunucudan gelen HTTP yanıtını temsil eder;
    • [response.text], bu yanıtta kapsüllenmiş belgenin metnini temsil eder. Hata ayıklama aşamasında, sunucunun bize ne gönderdiğini kontrol etmek yararlıdır;
  • 47. satır: [response.status_code], alınan yanıtın HTTP durum kodudur. Sunucumuz sadece üç tane gönderir:
    • 200 OK
    • 400 BAD REQUEST
    • 500 INTERNAL SERVER ERROR
  • 49. satır: Sunucumuz, hata durumunda bile her zaman jSON değerini gönderir. [response.json()] işlevi, alınan jSON dizesinden bir sözlük oluşturur. jSON dizesinin iki olası biçimini hatırlayalım:

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
  • 51-53. satırlar: durum kodu 200 değilse, yanıtta kapsüllenmiş hata mesajlarıyla bir istisna tetiklenir;
  • satır 56: vergi hesaplaması sonucunda oluşturulan sözlüğü alırız ve bunu [taxpayer] giriş parametresini güncellemek için kullanırız;

23.3.5. Çalıştırma

İstemciyi çalıştırmak için:

  • [server_03] sunucusunu, istediğiniz SGBD ile başlatın;
  • istemcinin [main] komut dosyasını çalıştırın;

Sonuçlar [data/output] klasöründe bulunacaktır. Bunlar, sürüm 5 ile aynıdır.

23.4. [dao] katmanının testleri

İstemci/sunucu uygulama mimarisine geri dönelim:

  • yazılan istemcide, [dao] ve [1] katmanlarının, [métier] ve [3] katmanlarıyla aynı arayüzü sunmasını sağladık. Bu nedenle, [4]'te, daha önce incelediğimiz |TestDaoMétier| test sınıfını kullanarak [métier] [3] katmanını test edeceğiz;

Test sınıfı aşağıdaki ortamda çalıştırılacaktır:

Image

  • [2] yapılandırması, az önce incelediğimiz [1] yapılandırmasıyla aynıdır;

[TestHttpClientDao] test sınıfı şöyledir:


import unittest


class TestHttpClientDao(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})
        dao.calculate_tax(taxpayer)
        # 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})
        dao.calculate_tax(taxpayer)
        # 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__':

    # uygulamayı yapılandırıyoruz
    import config
    config = config.configure({})

    # DAO katmanı
    dao = config['layers']['dao']

    # test yöntemleri çalıştırılıyor
    print("tests en cours...")
    unittest.main()

Bu sınıf, uygulamanın 4. sürümünde daha önce incelediğimiz |celle| sınıfına benzemektedir.

  • 40-41. satırlar: test ortamı yapılandırılır;
  • 44. satır: [dao] katmanına ilişkin bir referans alınır;
  • 47-48. satırlar: testler yürütülür;

Testleri çalıştırmak için bir |configuration d’exécution| oluşturulur:

Image

  • UnitTest testi için değil, bir konsol komut dosyası için bir yürütme yapılandırması oluşturulur;

Bu yapılandırmayı çalıştırdığımızda şu sonuçlar elde edilir:

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/http-clients/01/tests/TestHttpClientDao.py
tests en cours...
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
....
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
----------------------------------------------------------------------
Ran 11 tests in 0.130s

OK

Process finished with exit code 0

11 testin tümü başarılı oldu.