Skip to content

23. تمرین عملی: نسخهٔ ۶

23.1. مقدمه

اکنون به برنامه محاسبه مالیات خود بازمی‌گردیم. قصد داریم برنامه‌های وب مختلفی حول آن بسازیم.

در نسخهٔ ۵ تمرین عملی ما، داده‌های سازمان مالیاتی در یک پایگاه داده ذخیره شد. این نسخهٔ ۵ شامل دو برنامهٔ مجزا بود، اما آن‌ها برخی لایه‌های مشترک را به اشتراک می‌گذاشتند:

  • یک برنامه که مالیات را در حالت |batch| برای مالیات‌دهندگانی که در یک فایل متنی ثبت شده بودند، محاسبه می‌کرد؛
  • یک برنامه که مالیات را در حالت |تعاملی| برای مودعانی که جزئیاتشان از طریق صفحه‌کلید وارد می‌شد، محاسبه می‌کرد؛

نسخهٔ ۵ برنامهٔ محاسبهٔ مالیات دسته‌ای معماری زیر را داشت:

Image

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

Image

  • کلاینت وب [1] با سرور وب [2] ارتباط برقرار می‌کند، که به نوبه خود با SGBD و [3] ارتباط برقرار می‌کند؛
  • سرور وب [2] لایه‌های [métier]، [8]، [dao] و [9] را از برنامه اصلی حفظ می‌کند؛
  • برنامهٔ اصلی اسکریپت اصلی خود، [4]، و لایه‌های خود، [métier] و [15] را حفظ می‌کند. لایه‌های [métier]، [8] و [15] یکسان هستند؛
  • ارتباط کلاینت/سرور به دو لایه اضافی نیاز دارد:
    • لایه‌های [web] و [7] که اپلیکیشن وب را پیاده‌سازی می‌کنند؛
    • لایه [dao] [5] که به‌عنوان کلاینت برای اپلیکیشن وب [7] عمل می‌کند؛

در نسخه نهایی، محاسبه دسته‌ای مالیات به دو روش قابل انجام است:

  • منطق کسب‌وکار برای محاسبه مالیات توسط لایه سمت سرور [métier] انجام می‌شود. اسکریپت [main] از این روش استفاده خواهد کرد؛
  • منطق کسب‌وکار محاسبه مالیات توسط لایه سمت کلاینت [métier] انجام می‌شود. اسکریپت [main2] از این روش استفاده خواهد کرد؛

از این پس، ما چندین برنامهٔ کلاینت/سرور از نوع توصیف‌شدهٔ بالا را توسعه خواهیم داد که هر یک یکی یا چند فناوری جدید توسعهٔ وب را نشان می‌دهند.

23.2. سرور وب محاسبه مالیات

23.2.1. نسخه ۱

Image

اسکریپت [server_01] اپلیکیشن وب زیر است:

Image

  • در [1]، از یک URL پیکربندی‌شده استفاده می‌شود که سه مقدار به آن ارسال می‌گردد:
    • [marié] (بله/خیر) برای نشان دادن اینکه آیا مؤدی متأهل است؛
    • [enfants]: تعداد فرزندان مؤدی؛
    • [salaire]: حقوق سالانه مودی؛
  • در پاسخ به [2]، وب‌سرور یک رشته jSON را بازمی‌گرداند که مبلغ مالیات پرداختنی را به تفکیک اجزای مختلف آن نشان می‌دهد؛

معماری برنامه به شرح زیر است:

Image

  • مرورگر [1] از سرور [2] استعلام می‌گیرد. اسکریپت [server_01] لایه‌های [web] و [2] سرور را پیاده‌سازی می‌کند؛
  • لایه‌های [3-8] همان لایه‌هایی هستند که پیش‌تر در |نسخه ۵| اپلیکیشن محاسبه مالیات استفاده شده‌اند. ما آن‌ها را بدون تغییر مجدداً استفاده می‌کنیم؛
    • لایه‌های [métier] و [3] |اینجا| تعریف شده‌اند؛
    • لایه [dao] [4] |اینجا| تعریف شده است؛

برنامه وب [server_01] با استفاده از سه اسکریپت پیکربندی شده است:

  • [configکه کل برنامه را پیکربندی می‌کند؛
  • [config_database]، که دسترسی به پایگاه داده را پیکربندی می‌کند. ما با SGBD، MySQL و PostgreSQL کار خواهیم کرد؛
  • [config_layersکه لایه‌های برنامه را پیکربندی می‌کند؛

اسکریپت [config] به شرح زیر است:


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

    # مرحله ۱ ------
    # دایرکتوری این فایل
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # مسیر ریشه
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # وابستگی‌های مطلق
    absolute_dependencies = [
        # پوشه‌های پروژه
        #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",
        #ثابت‌ها، محدوده‌ها
        f"{root_dir}/impots/v05/entities",
        # IndexController
        f"{script_dir}/../controllers",
        # اسکریپت‌ها [config_database, config_layers]
        script_dir,
    ]
    # مسیر سیستم را تنظیم کنید
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # مرحله ۲ ------
    # پیکربندی برنامه
    # فهرست کاربران مجاز به استفاده از برنامه
    config['users'] = [
        {
            "login""admin",
            "password""admin"
        }
    ]

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

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

    #اعمال پیکربندی
    return config
  • تابع [configure] یک فرهنگ لغت [config] را به‌عنوان پارامتر (خط ۱) می‌پذیرد و پس از غنی‌سازی محتوای آن، آن را به‌عنوان نتیجه (خط ۵۴) بازمی‌گرداند. می‌شد مدت‌ها پیش اشاره کرد که نیازی به بازگرداندن نتیجه [config] نیست. در واقع، [config] یک مرجع دیکشنری است که کد فراخوان آن را با کد فراخوانی‌شونده به اشتراک می‌گذارد. بنابراین کد فراخوانی‌کننده از قبل این مرجع را در اختیار دارد (خط ۱) و بازگرداندن مجدد آن (خط ۵۴) بی‌فایده است. بنابراین، نوشتن:

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

تکراری است. کافی است بنویسید:


[module].configure(config) (2)

با این حال، من سبک اول نگارش را حفظ کرده‌ام زیرا فکر می‌کردم ممکن است بهتر نشان دهد که کد فراخوانی‌شونده در حال تغییر فرهنگ لغت [config] بود.

  • خط ۱: فرهنگ لغت [config] که توسط تابع [configure] دریافت شده است، دارای کلیدی به نام 'sgbd' است که مقدار آن از لیست [‘mysql’, ‘pgres’] گرفته می‌شود. [mysql] به این معنی است که پایگاه داده مورد استفاده توسط MySQL مدیریت می‌شود، در حالی که «pgres» به این معنی است که پایگاه داده مورد استفاده توسط PostgreSQL مدیریت می‌شود؛
  • خطوط ۴–۲۷: تمام پوشه‌هایی که حاوی عناصر مورد نیاز برنامه وب هستند، فهرست شده‌اند. این پوشه‌ها بخشی از مسیر پایتون (Python Path) برنامه را تشکیل خواهند داد (خطوط ۳۰–۳۱)؛
  • خطوط 33–40: تنها کاربران خاصی مجاز به دسترسی به برنامه خواهند بود. در اینجا، فهرستی حاوی یک کاربر واحد وجود دارد؛
  • خطوط ۴۳–۴۶: اسکریپت [config_database] پیکربندی پایگاه داده مورد استفاده را ایجاد می‌کند؛
  • خط ۴۶: پیکربندی تولید شده توسط اسکریپت [config_database] یک دیکشنری است که در پیکربندی عمومی تحت کلید «database» ذخیره می‌شود؛
  • خطوط ۴۸–۵۱: اسکریپت [config_layers] لایه‌های وب‌اپلیکیشن را نمونه‌سازی می‌کند. این اسکریپت یک دیکشنری را بازمی‌گرداند که در پیکربندی عمومی تحت کلید 'layers' ذخیره می‌شود؛

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

    # اطلاعات خاصی در یک دیکشنری ذخیره و بازگردانده می‌شود
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

اسکریپت [config_layers] لایه‌های سرور وب را پیکربندی می‌کند. ما در حال استفاده مجدد از یک |اسکریپت| هستیم که قبلاً با آن مواجه شده‌ایم:


def configure(config: dict) -> dict:
    # مصداق‌سازی لایه‌های کاربردی
    
    # DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)
    
    # منطق کسب‌وکار
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # نمونه‌های لایه در یک دیکشنری قرار می‌گیرند که به کد فراخوانی بازگردانده می‌شود
    return {
        "dao": dao,
        "métier": métier
    }
  • خط ۶: لایه [dao] با یک پایگاه داده پیاده‌سازی شده است؛
  • [ImpotsDaoWithAdminDataInDatabase] در |اینجا| تعریف شده است؛
  • [ImpôtsMétier] در |اینجا| تعریف شده است؛

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


# ما انتظار یک پارامتر MySQL یا PostgreSQL را داریم
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#وابستگی‌ها
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status

#بازیابی داده‌ها از مراجع مالیاتی
try:
    #`admindata` یک متغیر فقط-خواندنی در سطح برنامه خواهد بود
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# برنامه Flask
app = Flask(__name__)


# خانه URL: /?married=xx&children=yy&salary=zz
@app.route('/', methods=['GET'])
def index():
    # در ابتدا هیچ خطایی وجود ندارد
    erreurs = []
    #درخواست باید سه پارامتر در URL داشته باشد
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #وضعیت تأهل از URL بازیابی می‌شود
    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 استخراج می‌شود
    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 بازیابی می‌کند
    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؟
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # آیا خطایی وجود دارد؟
    if erreurs:
        #یک پاسخ خطا به کلاینت ارسال می‌شود
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    #خطایی وجود ندارد؛ می‌توانیم ادامه دهیم
    #محاسبه مالیات
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # پاسخ به کلاینت ارسال می‌شود
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


# فقط دستی
if __name__ == '__main__':
    # راه‌اندازی سرور Flask
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • خطوط ۱–۱۰: پارامترِ مشخص‌کنندهٔ SGBD مورد استفاده، بازیابی می‌شود؛
  • خطوط ۱۲–۱۴: با در دست داشتن این اطلاعات، می‌توان برنامه را پیکربندی کرد. به‌ویژه، مسیر پایتون ساخته می‌شود؛
  • خطوط ۱۶–۲۳: با استفاده از مسیر جدید پایتون، عناصر مورد نیاز را وارد می‌کنیم؛
  • خطوط ۲۵–۳۱: داده‌های مورد نیاز برای محاسبه مالیات را از سازمان مالیاتی بازیابی می‌کنیم؛
  • خطوط ۳۳–۳۴: نمونه‌سازی برنامه Flask؛
  • خط ۳۸: برنامه Flask تنها URL [/] را ارائه می‌دهد. این برنامه انتظار دارد URL را به صورت زیر [/ ?marié=xx&enfants=yy&salaire=zz] پیکربندی کرده باشد:
    • xx: بله / نه;
    • yy: تعداد فرزندان;
    • zz: حقوق سالانه؛
  • خطوط ۴۰–۸۹: اعتبار پارامترهای URL بررسی می‌شود؛
  • خط ۴۱: پیام‌های خطا در لیست [erreurs] انباشته می‌شوند؛
  • خط ۴۳: ممکن است به یاد داشته باشید که پارامترهای پیکربندی‌شده برای URL در [request.args] یافت می‌شوند (به |اینجا| مراجعه کنید):
    • شیء [request] همان شیء Flask است که در خط ۲۰ وارد شده است؛
    • شیء [request.args] مانند یک دیکشنری عمل می‌کند؛
  • خطوط ۴۳–۴۴: بررسی می‌کنیم که دقیقاً سه پارامتر وجود دارد (نه کمتر، نه بیشتر)؛
  • خطوط ۴۶–۴۹: بررسی می‌کنیم که پارامتر [marié] در URL موجود باشد؛
  • خطوط ۵۰–۵۴: اگر موجود باشد، بررسی می‌کنیم که مقدار کوچک‌نویس آن، پس از حذف فاصله‌های خالی ابتدایی و انتهایی، برابر با «yes» یا «no» باشد؛
  • خطوط ۵۶–۵۹: بررسی می‌کنیم که پارامتر [enfants] در URL موجود است؛
  • خطوط ۶۰–۶۶: اگر موجود باشد، بررسی کنید که مقدار آن یک عدد صحیح مثبت است؛
  • خط ۶۶: مهم است به خاطر داشته باشید که پارامترهای URL و مقادیر آن‌ها رشته‌های کاراکتری هستند. مقدار پارامتر [enfants] به یک عدد صحیح (int) تبدیل می‌شود؛
  • خطوط ۶۸–۷۸: برای پارامتر [salaire]، همان بررسی‌ها برای پارامتر [enfants] انجام می‌شود؛
  • خطوط ۸۱–۸۳: بررسی می‌شود تا اطمینان حاصل شود که در URL هیچ پارامتر دیگری به جز [‘marié, ‘enfants’, ‘salaire’] وجود ندارد؛
  • خطوط ۸۵–۸۹: اگر پس از تمام این بررسی‌ها، لیست [erreurs] خالی نبود، آنگاه این لیست خطاها به شکل یک رشته jSON و کد وضعیت [400 Bad Request] برای کلاینت ارسال می‌شود؛

از آنجا که بعداً اغلب نیاز خواهیم داشت یک رشته jSON را به عنوان پاسخ به کلاینت ارسال کنیم، چند خط مورد نیاز برای این کار به ماژول [myutils.py] که قبلاً از آن استفاده کرده‌ایم، منتقل شده است:

Image

اسکریپت [myutils.py] به صورت زیر درمی‌آید:


# واردات‌ها
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    #absolute_dependencies: فهرستی از نام‌های مطلق پوشه‌ها

    ….


#تولید پاسخ HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    # بدنه پاسخ HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    # بدنه پاسخ HTTP از jSON است
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    #پاسخ HTTP ارسال می‌شود
    return response, status_code
  • خط ۱۶: تابع [json_response] منتظر دو پارامتر است:
    • [réponse]: دیکشنری حاوی رشته jSON که باید به کلاینت وب ارسال شود؛
    • [status_code]: کد وضعیت HTTP پاسخ؛
  • خط ۱۸: بدنه پاسخ روی jSON تنظیم می‌شود؛
  • خط ۲۰: هدر HTTP اضافه می‌شود که به کلاینت وب اطلاع می‌دهد که jSON را دریافت خواهد کرد؛
  • خط ۲۲: پاسخ HTTP به کد فراخوانی ارسال می‌شود. ارسال آن به کلاینت وب بر عهده کد فراخوانی است؛

فایل [__init__.py] به شرح زیر تغییر می‌کند:


from .myutils import set_syspath, json_response

نسخه جدید [myutils] با استفاده از دستور [pip install .] در ترمینال PyCharm در میان ماژول‌های در سطح ماشین نصب می‌شود:


(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
  • خط ۱: برای وارد کردن این فرمان باید در پوشه [packages] باشید؛

کد اسکریپت [server_01] به شرح زیر ادامه دارد:



    # آیا خطایی وجود دارد؟
    if erreurs:
        #یک پاسخ خطا به کلاینت ارسال می‌شود
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    #خطایی وجود ندارد، می‌توانیم ادامه دهیم
    #محاسبه مالیات
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    #ارسال پاسخ به کلاینت
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)

  • خط ۱۰: در این نقطه، پارامترهای مورد انتظار در URL موجود و صحیح هستند؛
  • خط ۱۰: شیء [TaxPayer] که مودی مالیاتی را مدل می‌کند، ایجاد می‌شود؛
  • خط ۱۱: به لایه [métier] دستور داده می‌شود که مالیات را محاسبه کند. توجه داشته باشید که عناصری که توسط لایه [métier] محاسبه می‌شوند، در شی [taxpayer] که به عنوان پارامتر ارسال شده است، درج می‌شوند؛
  • خط ۱۳: پاسخ در قالب یک رشته jSON به کلاینت وب ارسال می‌شود. این رشته jSON از یک فرهنگ لغت است. وابسته به کلید [result]، فرهنگ لغت شیء [taxpayer] در آنجا ذخیره می‌شود. ما نمی‌توانستیم خود شیء [taxpayer] را ذخیره کنیم، زیرا نمی‌توان آن را به jSON سریالیزه کرد؛

ما دو پیکربندی اجرای ایجاد می‌کنیم، یکی برای MySQL و دیگری برای PostgreSQL:

Image

در اینجا چند نمونه از خروجی آورده شده است (شما برنامه [server_01] و SGBD را اجرا کرده و سپس با استفاده از یک مرورگر، http://localhost:5000/ را درخواست کرده‌اید):

Image

Image

در اینجا مثالی از خروجی در کنسول Postman آورده شده است:

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"]}}
  • خط ۱: یک URL نادرست درخواست شده است؛
  • خط ۱۰: سرور با کد وضعیت ۴۰۰ پاسخ می‌دهد BAD REQUEST;

23.2.2. نسخه ۲

Image

نسخهٔ ۲ سرور پردازش URL را در ماژول [index_controller] [5] ایزوله می‌کند:


# وارد کردن وابستگی‌ها
import re

from flask_api import status
from werkzeug.local import LocalProxy


#URL پیکربندی شده به عنوان: /?married=xx&children=yy&salary=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # وابستگان
    from TaxPayer import TaxPayer

    # در ابتدا هیچ خطایی وجود ندارد
    erreurs = []
    # پرس‌وجو باید سه پارامتر داشته باشد
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #وضعیت تأهل URL را بازیابی می‌کند
    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
    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 بازیابی می‌کند
    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 وجود دارد؟
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # آیا خطایی وجود دارد؟
    if erreurs:
        #یک پاسخ خطا به مشتری ارسال می‌شود
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    #خطایی وجود ندارد، می‌توانیم ادامه دهیم
    #محاسبه مالیات
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    #ارسال پاسخ به کلاینت
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • خط ۹: تابع [execute] دو پارامتر می‌گیرد:
    • [request]: درخواست مشتری HTTP;
    • [config]: فرهنگ لغت پیکربندی برنامه؛

اسکریپت [server_02] به شرح زیر است:


# در انتظار پارامتر MySQL یا PostgreSQL
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

# وابستگی‌ها
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

# بازیابی داده‌ها از مراجع مالیاتی
try:
    #`admindata` یک متغیر فقط-خواندنی در سطح برنامه خواهد بود
    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
app = Flask(__name__)


# خانه URL: /?married=xx&child=yy&salary=zz
@app.route('/', methods=['GET'])
def index():
    #درخواست اجرا می‌شود
    résultat, statusCode = index_controller.execute(request, config)
    #پاسخ ارسال می‌شود
    return json_response(résultat, statusCode)


#فقط تابع اصلی
if __name__ == '__main__':
    # سرور راه‌اندازی می‌شود
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • خطوط ۳۶–۴۱: پردازش مسیر /;
  • خط ۳۹: استفاده از تابع [IndexController.execute

اکنون از این تکنیک استفاده خواهیم کرد: هر مسیر توسط ماژول مخصوص به خود پردازش خواهد شد.

نتایج اجرا همانند نسخهٔ ۱ است.

23.2.3. نسخه ۳

Image

نسخهٔ ۳ مفهوم احراز هویت را معرفی می‌کند.

اسکریپت [server_03] به صورت زیر درمی‌آید:


#منتظر یک پارامتر MySQL یا PostgreSQL باشید
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#وابستگی‌ها
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller

# استخراج داده‌ها از مراجع مالیاتی
try:
    #config[‘admindata’] یک متغیر در سطح برنامه خواهد بود که فقط قابل خواندن است
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# مدیر احراز هویت
auth = HTTPBasicAuth()


# روش احراز هویت
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    # فهرست کاربران
    users = config['users']
    # این لیست پیمایش می‌شود
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # یافت نشد
    return False


# برنامه Flask
app = Flask(__name__)


# خانه URL: /?married=xx&child=yy&salary=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    # ما درخواست را اجرا می‌کنیم
    résultat, statusCode = index_controller.execute(request, config)
    #پاسخ ارسال می‌شود
    return json_response(résultat, statusCode)


#فقط تابع اصلی
if __name__ == '__main__':
    # سرور راه‌اندازی شد
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • خط ۲۱: یک دستگیرکننده احراز هویت وارد می‌شود. انواع مختلفی از احراز هویت با یک وب‌سرور وجود دارد. نوع احراز هویتی که ما اینجا استفاده می‌کنیم [HTTP Basic] نام دارد. هر نوع احراز هویت از یک دیالوگ خاص کلاینت/سرور پیروی می‌کند؛
  • خط ۳۳: یک نمونه از دست‌کارگر احراز هویت ایجاد می‌شود؛
  • خط ۳۷: تگ [@auth.verify_passwordتابع را برای زمانی که دستگیرکننده احراز هویت بخواهد نام کاربری و رمز عبور ارسال‌شده توسط کلاینت را مطابق پروتکل [HTTP Basic] تأیید کند، مشخص می‌کند؛
  • خط ۵۵: حاشیه‌نویسی [@auth.login_required] مسیری را مشخص می‌کند که در آن کلاینت وب باید احراز هویت شود. اگر کلاینت وب هنوز اعتبارنامه‌های خود را ارسال نکرده باشد، سرور وب به طور خودکار با استفاده از پروتکل پایه HTTP آنها را درخواست خواهد کرد؛

ماژول [flask_httpauth] باید نصب شود:


(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 چه اتفاقی می‌افتد. شما:

  • یک پیکربندی اجرا ایجاد کنید؛
  • برنامه وب را راه‌اندازی کنید؛
  • نسخهٔ SGBD مورد نظر خود را اجرا کنید؛
  • با استفاده از Postman، URL و [/] را درخواست کنید؛

گفت‌وگوی کلاینت/سرور در کنسول Postman به شرح زیر است:

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
  • خط ۱۰: سرور پاسخ می‌دهد که ما مجاز به دسترسی به URL [/] نیستیم؛
  • خط ۱۳: به ما می‌گوید از کدام پروتکل احراز هویت استفاده کنیم، در این مورد احراز هویت پایه؛

امکان پیکربندی Postman برای ارسال اطلاعات کاربری با استفاده از پروتکل احراز هویت پایه وجود دارد:

Image

  • در [6-7]، اعتبارنامه‌های موجود در اسکریپت [config] را وارد می‌کنیم: Image

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

گفتگوی کلاینت/سرور در کنسول Postman به شکل زیر درمی‌آید:


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"]}}
  • خط ۲: کلاینت Postman اعتبارنامه‌های کاربر را برای [admin / admin] به صورت رمزگذاری‌شده ارسال می‌کند؛
  • خط ۱۷: سرور به درستی پاسخ می‌دهد. این خطاها را گزارش می‌کند زیرا پارامترهای [marié, enfants, salaire] (خط ۱) ارسال نشده‌اند، اما خطای احراز هویت را گزارش نمی‌کند؛

اکنون با استفاده از یک مرورگر (در مثال زیر فایرفاکس) درخواست URL را انجام می‌دهیم:

Image

  • مانند Postman، فایرفاکس پاسخ HTTP را از سرور با سربرگ‌های HTTP دریافت کرد:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

فایرفاکس، مانند سایر مرورگرها، پس از دریافت این سربرگ‌ها، دیالوگ را متوقف نمی‌کند. این مرورگر از کاربر می‌خواهد تا نام کاربری و رمز عبور مورد درخواست سرور را وارد کند. در مثال بالا، صرفاً تایپ کردن «admin / admin» برای دریافت پاسخ سرور کافی است:

Image

23.3. کلاینت وب برای سرور محاسبه مالیات

23.3.1. مقدمه

در بخش قبلی، کلاینت وب برای سرور محاسبه مالیات یک مرورگر بود. در این بخش، کلاینت وب یک اسکریپت کنسول خواهد بود. معماری به شرح زیر است:

Image

  • کلاینت وب از لایه‌های [1-2] تشکیل شده است؛
  • سرور وب از لایه‌های [3-9] تشکیل شده است. همان‌طور که در بخش قبلی ذکر شد؛

بنابراین ما باید لایه‌های [1-2] را بنویسیم.

لایه [dao] [2] باید قادر به برقراری ارتباط با سرور وب [3] باشد. اکنون با پروتکل HTTP آشنا هستیم و می‌توانیم، برای مثال، از ماژول [pycurl] که قبلاً مطالعه کرده‌ایم استفاده کنیم تا اسکریپتی بنویسیم که با سرور وب [3] ارتباط برقرار کند. با این حال، ماژول‌هایی وجود دارند که در دیالوگ‌های کلاینت/سرور HTTP تخصص دارند. ما قصد داریم از یکی از آن‌ها، ماژول [requests]، استفاده کنیم:


(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

ساختار دایرکتوری برای اسکریپت‌های کلاینت وب به شرح زیر است:

Image

این اسکریپت، برنامه محاسبه مالیات حالت دسته‌ای را که در |نسخه ۱| توضیح داده شده است، پیاده‌سازی می‌کند. آخرین نسخه این برنامه |نسخه ۵| است. در اینجا یادآوری نحوه عملکرد آن آمده است:

  • مالیات‌دهندگانِ مورد محاسبه در فایل متنی [taxpayersdata.txt] فهرست شده‌اند:
# داده‌های معتبر: id, married, children, salary
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
# رده‌های نادرست ایجاد می‌شوند
#مقادیر ناکافی
11,12
# مقادیر نادرست
x,x,x,x
  • نتایج در دو فایل نوشته می‌شوند:
  • فایل متنی [errors.txt] شامل خطاهای شناسایی‌شده در فایل مودیان است:

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]
  • (ادامه)
    • فایل jSON [résultats.json] شامل نتایج محاسبه مالیات برای مالیات‌دهندگان مختلف است:

[
  {
    "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. پیکربندی کلاینت وب

Image

پیکربندی با استفاده از دو اسکریپت انجام می‌شود:

  • [configکه تمام پیکربندی خارج از لایه‌های معماری را مدیریت می‌کند؛
  • [config_layers]، که پیکربندی لایه‌های معماری را انجام می‌دهد؛

اسکریپت [config] به شرح زیر است:


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

    # مرحله ۱ ------

    #پوشه این فایل
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # مسیر ریشه
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # وابستگی‌های مطلق
    absolute_dependencies = [
        # پوشه‌های پروژه
        #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",
        #ثابت‌ها، محدوده‌ها
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # اسکریپت‌های پیکربندی
        script_dir,
    ]

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

    # مرحله ۲ ------
    #پیکربندی برنامه با ثابت‌ها
    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"
            }
        }
    }
    )

    # مرحله ۳ ------
    # مثال‌زدایی لایه‌ها
    import config_layers
    config['layers'] = config_layers.configure(config)

    # اعمال پیکربندی
    return config
  • خط ۱: تابع [configure] دیکشنری‌ای را که باید با اطلاعات پیکربندی پر شود، به عنوان پارامتر می‌پذیرد. این دیکشنری ممکن است از قبل پر شده باشد یا خالی باشد. در اینجا، خالی خواهد بود؛
  • خطوط ۴۰–۴۲: مسیرهای مطلق سه فایل متنی که توسط لایه [dao] مدیریت می‌شوند؛
  • خطوط ۴۳–۵۰: مرتبط با کلید [server]، اطلاعاتی که لایه [dao] باید در مورد وب‌سروری که باید با آن ارتباط برقرار کند، بداند:
    • خط ۴۴: کلید URL سرویس وب؛
    • خط ۴۵: کلید [authBasic] در صورتی که دسترسی به URL نیازمند احراز هویت پایه‌ای باشد، روی True تنظیم می‌شود؛
    • خطوط ۴۶–۴۹: اعتبارنامه‌های کاربری که در صورت نیاز به احراز هویت، احراز هویت خواهد شد؛
  • خطوط ۵۶–۵۷: لایه‌ها نمونه سازی می‌شوند – در این مورد، لایهٔ تکی [dao] – و ارجاعات لایه در [config] قرار می‌گیرند که با کلید [layers] مرتبط است؛

اسکریپت [config_layers] به شرح زیر است:


def configure(config: dict) -> dict:
    # آشکارسازی لایه‌های برنامه

    #لایه DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # اعمال پیکربندی لایه
    return {
        "dao": dao
    }
  • خط ۱: تابع [configure] دیکشنری‌ای را که برنامه را پیکربندی می‌کند دریافت می‌کند؛
  • خطوط ۴–۶: لایه [dao] ایجاد می‌شود. در خط ۶، پیکربندی برنامه به آن ارسال می‌شود که از آن اطلاعات مورد نیاز خود را بازیابی خواهد کرد؛
  • خطوط ۸–۱۱: یک دیکشنری بازگردانده می‌شود که حاوی مرجع لایه [dao] است؛

23.3.3. اسکریپت اصلی [main]

اسکریپت اصلی [main] نسخه‌ای از آن در |نسخه ۵| است:


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

#وابستگی‌ها
from ImpôtsError import ImpôtsError

# کد
try:
    #بازیابی لایه [dao]
    dao = config["layers"]["dao"]
    # خواندن داده‌های مودی
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # برای مالیات‌دهندگان؟
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #محاسبه مالیات مودیان
    for taxpayer in taxpayers:
        # 'مالیات‌دهنده' هم پارامتر ورودی و هم پارامتر خروجی است
        # 'مالیات‌دهنده' اصلاح خواهد شد
        dao.calculate_tax(taxpayer)
    # نوشتن نتایج در یک فایل متنی
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    #نمایش خطا
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # تکمیل شد
    print("Travail terminé...")
  • خطوط ۲–۳: برنامه پیکربندی می‌شود؛
  • خط ۱۳: لایه [dao] فهرست مودعانی را که مالیات برای آن‌ها باید محاسبه شود، فراهم می‌کند؛
  • خط ۲۱: لایه [dao] مالیات هر یک از آنها را محاسبه می‌کند؛
  • خط ۲۳: نتایج در فایلی به نام jSON ذخیره می‌شوند؛

23.3.4. پیاده‌سازی لایه [dao]

Image

بیایید معماری کلاینت/سرور مورد استفاده را مجدداً بررسی کنیم:

Image

  • در [2, 6]، می‌توانیم ببینیم که لایه [dao] دو نقش دارد:
    • این لایه به سیستم فایل دسترسی دارد تا هم داده‌های مودیان را بخواند و هم نتایج محاسبات مالیاتی را بنویسد. ما از قبل یک کلاس |AbstractImpôtsDao| داریم که می‌تواند این کار را انجام دهد. این کلاس از |نسخه ۴| به کار گرفته شده است؛
    • با سرور وب [3] ارتباط برقرار می‌کند؛

در |نسخه ۵|، اسکریپت اصلی [main] [1] مستقیماً با لایه [métier] [4] ارتباط برقرار می‌کرد. ما ترجیح می‌دهیم این اسکریپت را تغییر ندهیم. برای این کار، اطمینان حاصل خواهیم کرد که لایه [dao] [2] رابط لایه [métier] [4] را پیاده‌سازی کند. به این ترتیب، اسکریپت اصلی [main] به نظر می‌رسد که مستقیماً با لایه [métier] [4] در ارتباط است و می‌تواند این واقعیت را که این لایه روی یک ماشین متفاوت قرار دارد، به طور کامل نادیده بگیرد.

تعریف کلاسی که لایه‌های [dao] و [2] را پیاده‌سازی می‌کند می‌تواند به شکل زیر باشد:


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • کلاس [ImpôtsDaoWithHttpClient]:
    • از کلاس [AbstractImpôtsDao] ارث می‌برد، که به آن امکان می‌دهد ارتباط با سیستم فایل [6] را مدیریت کند؛
    • رابط [InterfaceImpôtsMétier] را پیاده‌سازی می‌کند تا مجبور نباشد اسکریپت اصلی [main] را از |نسخه ۵| تغییر دهد؛

کد کامل کلاس [ImpôtsDaoWithHttpClient] به شرح زیر است:


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

    # سازنده
    def __init__(self, config: dict):
        # ابتدایی‌سازی والد
        AbstractImpôtsDao.__init__(self, config)
        # ذخیره‌سازی پارامتر
        self.__config_server = config["server"]

    #روش بلااستفاده از [AbstractImpôtsDao]
    def get_admindata(self) -> AdminData:
        pass

    #محاسبه مالیات
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        # اجازه داده می‌شود استثناءها propagate شوند
        #پارامترهای GET
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # اتصال با استفاده از احراز هویت پایه؟
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL از سرور پرس‌وجو شده
                self.__config_server['urlServer'],
                #پارامترهای URL
                params=params,
                # احراز هویت پایه
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            # اتصال بدون احراز هویت پایه
            response = requests.get(self.__config_server['urlServer'], params=params)
        #اعتبارسنجی
        print(response.text)
        #کد وضعیت پاسخ HTTP
        status_code = response.status_code
        #پاسخ jSON در یک دیکشنری ذخیره می‌شود
        résultat = response.json()
        #خطا اگر کد وضعیت 200 نباشد OK
        if status_code != status.HTTP_200_OK:
            # ما می‌دانیم که خطاها با کلید پاسخ [erreurs] مرتبط شده‌اند
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # معلوم است که نتیجه با کلید [result] در پاسخ مرتبط بود
        #پارامتر ورودی با استفاده از این نتیجه تغییر داده می‌شود
        taxpayer.fromdict(résultat["réponse"]["result"])
  • خطوط ۲۱–۲۳: کلاس [AbstractImpôtsDao] (خط ۱۲) یک متد انتزاعی [get_admindata] دارد. ما موظف به پیاده‌سازی آن هستیم حتی اگر از آن استفاده نکنیم (داده‌های مدیریتی توسط سرور مدیریت می‌شوند، نه توسط کلاینت)؛
  • خط ۲۶: متد [calculate_tax] متعلق به اینترفیس [InterfaceImpôtsMétier] (خط ۱۲) است. ما باید آن را پیاده‌سازی کنیم؛
  • خط ۱۵: سازنده (constructor) تنها واژه‌نامه پیکربندی برنامه را به‌عنوان پارامتر خود می‌گیرد؛
  • خطوط ۱۶–۱۷: کلاس والد [AbstractImpôtsDao] نیز با ارسال پیکربندی برنامه برای آن، مقداردهی اولیه می‌شود. این کلاس نام سه فایل متنی را که باید با آن‌ها کار کند، در همین پیکربندی پیدا خواهد کرد؛
  • خطوط ۱۸–۱۹: اطلاعاتی که مربوط به وب سرور مورد استفاده برای محاسبه مالیات است، به‌صورت محلی درون کلاس ذخیره می‌شود؛
  • خط ۲۶: متد [calculate_tax] یک شیء از نوع |Taxpayer| را به‌عنوان پارامتر می‌پذیرد. برای مطابقت با امضای متد [InterfaceImpôtsMétier.calculate_tax]، همچنین پارامتر [admindata] را می‌پذیرد که هدف آن دربرگرفتن داده‌های مرجع مالیاتی است. در سمت کلاینت، ما این داده‌ها را نداریم. این پارامتر همیشه به صورت [None] باقی می‌ماند. این پیچیدگی نشان می‌دهد که کلاس [ImpôtsMétier] در اصل به اشتباه نوشته شده است:
  • امضای [calculate_tax] باید به سادگی به این صورت می‌بود:

def calculate_tax(self, taxpayer: TaxPayer)

و پارامتر [admindata : AdminData] باید به سازنده کلاس ارسال می‌شد؛

  • خط ۲۷: کد متد [calculate_tax] در یک بلوک try / catch / finally قرار نگرفته است. این بدان معناست که هیچ استثنایی مدیریت نخواهد شد و به کد فراخوانی‌کننده، در این مورد اسکریپت [main]، منتقل خواهد شد. این اسکریپت تمام استثناهای منتقل‌شده از لایه [dao] را می‌گیرد؛
  • خط ۲۸: محاسبه مالیات در سمت سرور انجام می‌شود. بنابراین ما باید با سرور ارتباط برقرار کنیم. این کار با استفاده از ماژول [requests] که در خط ۲ وارد شده است، انجام می‌شود؛
  • خطوط ۳۱–۴۳: برای ارسال یک درخواست GET به سرور وب، از متد [requests.get] استفاده می‌کنیم:
    • خطوط ۳۳–۳۴: اولین پارامتر این متد، URL است که باید با آن تماس گرفته شود؛
    • خطوط ۳۵–۴۰: دو پارامتر دیگر، پارامترهای نامی هستند و ترتیب آن‌ها اهمیتی ندارد؛
    • خطوط ۳۵–۳۶: مقدار پارامتر نامی [params] باید یک دیکشنری حاوی اطلاعاتی باشد که قرار است در URL در قالب [/url ?param1=valeur1&param2=valeur2&…] گنجانده شود؛
    • خط ۲۹: دیکشی که حاوی سه پارامتر [marié, enfants, salaire] است که وب‌سرور منتظر آن است. ما نیازی به نگرانی در مورد رمزگذاری (معروف به urlencoded) که این پارامترها باید پشت سر بگذارند، نداریم. [requests] این کار را انجام می‌دهد؛
    • خطوط ۳۷–۴۰: پارامتر با نام [auth] یک توییپ (tuple) از دو عنصر (نام کاربری، رمز عبور) است. این نشان‌دهنده اعتبارنامه‌ها برای احراز هویت پایه‌ای است؛
  • خطوط 44–45: این دو خط فقط برای اهداف آموزشی هستند (پس از اتمام اشکال‌زدایی، کامنت خواهند شد):
    • [response] نماینده پاسخ سرور HTTP است؛
    • [response.text] نمایانگر متن سند گنجانده‌شده در این پاسخ است. در طول مرحله اشکال‌زدایی، بررسی اینکه سرور چه چیزی را برای ما ارسال کرده است، مفید است؛
  • خط ۴۷: [response.status_code] کد وضعیت HTTP پاسخ دریافتی است. سرور ما تنها سه مورد از این‌ها را ارسال می‌کند:
    • 200 OK
    • 400 BAD REQUEST
    • ۵۰۰ INTERNAL SERVER ERROR
  • خط ۴۹: سرور ما همیشه jSON را ارسال می‌کند، حتی در صورت خطا. تابع [response.json()] یک فرهنگ لغت از رشته دریافتی jSON ایجاد می‌کند. بیایید دو شکل ممکن برای رشته jSON را به یاد بیاوریم:

{"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}}}
  • خطوط ۵۱–۵۳: اگر کد وضعیت ۲۰۰ نباشد، یک استثنا با پیام‌های خطایی که در پاسخ قرار گرفته‌اند، پرتاب می‌شود؛
  • خط ۵۶: دیکشنری تولیدشده توسط محاسبه مالیات بازیابی شده و برای به‌روزرسانی پارامتر ورودی [taxpayer] استفاده می‌شود؛

23.3.5. اجرا

برای اجرای کلاینت:

  • سرور [server_03] را با SGBD دلخواه خود راه‌اندازی کنید؛
  • اسکریپت کلاینت [main] را اجرا کنید؛

نتایج در پوشه [data/output] یافت می‌شوند. این نتایج مشابه نسخه ۵ هستند.

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

بیایید به معماری برنامهٔ کلاینت/سرور بازگردیم:

  • در کد کلاینت، ما اطمینان حاصل کرده‌ایم که لایه [dao] [1] همان رابط را مانند لایه [métier] [3] فراهم می‌کند. بنابراین، در [4]، از کلاس تست |TestDaoMétier| که پیش‌تر بررسی کرده‌ایم، برای آزمون لایه [métier] [3] استفاده خواهیم کرد؛

کلاس تست در محیط زیر اجرا خواهد شد:

Image

  • پیکربندی [2] با پیکربندی [1] که همین حالا بررسی کردیم، یکسان است؛

کلاس تست [TestHttpClientDao] به شرح زیر است:


import unittest


class TestHttpClientDao(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})
        dao.calculate_tax(taxpayer)
        #تأیید
        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})
        dao.calculate_tax(taxpayer)
        # بررسی‌ها
        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__':

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

    #لایه DAO
    dao = config['layers']['dao']

    # اجرای روش‌های آزمون
    print("tests en cours...")
    unittest.main()

این کلاس مشابه کلاسی است که پیش‌تر در نسخهٔ ۴ برنامه بررسی شده است.

  • خطوط ۴۰–۴۱: محیط تست پیکربندی می‌شود؛
  • خط ۴۴: مرجعی به لایه [dao] بازیابی می‌شود؛
  • خطوط ۴۷–۴۸: تست‌ها اجرا می‌شوند؛

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

Image

  • ما یک پیکربندی اجرای (run configuration) برای یک اسکریپت کنسول ایجاد می‌کنیم، نه برای یک تست UnitTest؛

وقتی این پیکربندی اجرا می‌شود، نتایج زیر به دست می‌آیند:

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

تمام ۱۱ تست با موفقیت اجرا شدند.