توسعه بک‌اند با FastAPI فهرست مطالب
فصل ۱۶

پروژه پایانی: یکپارچه‌سازی، رفع باگ، بازنویسی و بازبینی کد

امروز درس تازه‌ای نیست. API آموزشگاه را در وضعیتی تحویل می‌گیرید که هر برنامه‌نویسی می‌شناسد: تقریباً تمام. چند آزمون شکست می‌خورند، چند باگ هیچ آزمونی ندارند، دو endpoint جا مانده‌اند، یک endpoint زشت است و کلاینت عقب مانده است. در گروه‌های دونفره همه آنچه در این دوره یاد گرفتید را به کار می‌برید تا پروژه درست، کامل و تمیز شود، و بعد از کدتان در یک جلسه بازبینی دفاع می‌کنید.

در این فصل می‌خوانید
  1. برنامه نهایی چه دارد
  2. تقسیم زمان جلسه
  3. شروع کار
  4. یک راه‌حل
  5. بازبینی کد
  6. از اینجا به بعد
  7. خلاصه دوره

۱. برنامه نهایی چه دارد

لایهآنچه در این دوره ساختیمفصل
چارچوبFastAPI، روترها، مستند زنده۲
داده ورودی و خروجیطرح‌واره‌های Pydantic، Create و Update و Read۳
ساختارتزریق وابستگی، سرویس‌ها، تنظیمات، خطای متمرکز۴
پایگاه دادهمدل‌های تایپ‌دار SQLAlchemy 2، رابطه‌ها۵ تا ۷
تحول طرحمهاجرت‌های Alembic ۰۰۰۱ تا ۰۰۰۴۸
فهرست حرفه‌ایصفحه‌بندی، جست‌وجو، فیلتر، مرتب‌سازی۸
امنیتArgon2، JWT، نقش‌ها، قاعده مالکیت۹ و ۱۰
قابلیت‌های واقعیبارگذاری فایل، CORS، middleware۱۱
کاراییasync از بالا تا پایین۱۲
اطمینانمجموعه آزمون pytest۱۳
مصرف‌کنندهکلاینت پایتونی و مجموعه Bruno۱۴ و ۱۵

۲. تقسیم زمان جلسه

بخشدقیقهآنچه اتفاق می‌افتد
توجیه۱۰خواندن TASKS.md و تشکیل گروه‌ها
کار عملی۱۲۵وظیفه‌های A تا E
بازبینی۴۵ارائه گروه‌ها و پرسش‌های code-review.md

۳. شروع کار

cd S16-capstone/01_starting_project
alembic upgrade head
python seed.py
pytest                 # 10 tests fail: your first to-do list
python main.py         # and open bruno/ in Bruno

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

A. سبز کردن مجموعه آزمون

  1. «فهرست دوره‌ها خالی است، در حالی که دوره داریم.»
  2. «دوره‌ای با ظرفیت دو، دانشجوی سوم را پذیرفت.»
  3. «پاسخ ثبت‌نام چیزی را نشان می‌دهد که هرگز نباید نشان دهد.»
  4. «GET /users/me/enrollments با ۵۰۰ از کار می‌افتد.»
  5. «رمزهایی مثل 12345678 پذیرفته می‌شوند.»
  6. «یک مدرس یک دوره را حذف کرد.»
  7. «هر مدرس واردشده‌ای دانشجویان همه دوره‌ها را می‌بیند.»

B. باگ‌هایی که هنوز آزمونی ندارند

  1. «فیلتر min_price=3000000 دوره‌های ارزان را نشان می‌دهد.»
  2. «کاربری را غیرفعال کردیم، اما برنامه‌اش تا انقضای توکن کار می‌کند.»
  3. «دانشجویی که ثبت‌نامش را لغو کرده هنوز فایل‌های درسی را دانلود می‌کند.»
  4. «یک مدرس ظرفیت دوره را منفی سه گذاشت.»

برای هر کدام اول یک آزمون بنویسید که شکست بخورد، بعد باگ را درست کنید و ببینید آزمون سبز می‌شود.

C. قابلیت‌های جامانده

  1. PATCH /users/me: کاربر نام کامل خودش را تغییر دهد، هرگز نقش یا is_active را.
  2. POST /users/me/password: رمز قدیمی اشتباه ۴۰۱، رمز تازه ضعیف ۴۲۲ و موفقیت ۲۰۴.

D. بازنویسی

  1. GET /users/stats کار می‌کند، اما نگاهش کنید: منطق کسب‌وکار در روتر، کل جدول‌ها در پایتون و حلقه‌های تودرتو. آن را به سرویس ببرید و بگذارید پایگاه داده با GROUP BY بشمارد. پاسخ JSON نباید تغییر کند.
@router.get('/stats')
async def stats(admin: AdminUser, db: DbSession) -> dict:
    # REFACTOR ME (capstone): business logic in a router, Python loops over whole tables
    users = (await db.scalars(select(User))).all()
    roles = {}
    for u in users:
        if u.role in roles:
            roles[u.role] = roles[u.role] + 1
        else:
            roles[u.role] = 1
    courses = (await db.scalars(select(Course))).all()
    active = 0
    for c in courses:
        if c.is_active == True:  # noqa: E712
            active += 1
    enrollments = (await db.scalars(select(Enrollment))).all()
    statuses = {}
    for e in enrollments:
        statuses[e.status] = statuses.get(e.status, 0) + 1
    counts = {}
    for e in enrollments:
        if e.status == 'active':
            for c in courses:
                if c.id == e.course_id:
                    counts[c.title] = counts.get(c.title, 0) + 1
    top = sorted(counts.items(), key=lambda x: -x[1])[:3]
    return {
        'users_per_role': roles,
        'active_courses': active,
        'enrollments_per_status': statuses,
        'top_courses': [{'title': t, 'students': n} for t, n in top],
    }

کدی که باید بازنویسی شود: 01_starting_project/app/routers/users.py

E. آزمون و کلاینت

  1. برای هر باگ بخش B و هر قابلیت بخش C آزمون بنویسید.
  2. به client_app متدهای update_me، change_password، my_enrollments و enroll را با آزمون MockTransport اضافه کنید، به‌علاوه نمای دانشجو در داشبورد.
روش پیشنهادی

برای هر وظیفه: نشانه را در Bruno بازسازی کنید، آزمونی بنویسید که همان نشانه را نشان دهد، کد را درست کنید و کل مجموعه را دوباره اجرا کنید. هیچ درستی بدون آزمون پذیرفته نیست.

۴. یک راه‌حل

پوشه 02_solution یک راه‌حل کامل است که همه ۶۹ آزمونش سبزند. فقط بعد از بازبینی بازش کنید و با کار خودتان مقایسه کنید.

    async def stats(self) -> Stats:
        """Every number is computed by the DATABASE (GROUP BY), not by Python loops."""
        roles = await self.db.execute(select(User.role, func.count()).group_by(User.role))
        statuses = await self.db.execute(
            select(Enrollment.status, func.count()).group_by(Enrollment.status)
        )
        active_courses = await self.db.scalar(
            select(func.count()).select_from(Course).where(Course.is_active)
        )
        top = await self.db.execute(
            select(Course.title, func.count(Enrollment.id).label('students'))
            .join(Enrollment, Enrollment.course_id == Course.id)
            .where(Enrollment.status == 'active')
            .group_by(Course.id)
            .order_by(func.count(Enrollment.id).desc(), Course.title)
            .limit(3)
        )
        return Stats(
            users_per_role=dict(roles.tuples().all()),
            active_courses=active_courses or 0,
            enrollments_per_status=dict(statuses.tuples().all()),
            top_courses=[{'title': title, 'students': n} for title, n in top.tuples()],
        )

آمار با GROUP BY: 02_solution/app/services.py

    async def update_me(self, user: User, data: UserUpdateMe) -> User:
        user.full_name = data.full_name
        await self.db.commit()
        await self.db.refresh(user)
        return user

    async def change_password(self, user: User, data: PasswordChange) -> None:
        valid, _ = verify_password(data.old_password, user.hashed_password)
        if not valid:
            raise AuthenticationError('The old password is not correct')
        if data.old_password == data.new_password:
            raise BusinessRuleError('The new password must be different')
        user.hashed_password = hash_password(data.new_password)
        await self.db.commit()

تغییر نام و رمز: 02_solution/app/services.py

async def test_disabled_user_token_stops_working(
    client: AsyncClient, make_user: UserFactory, auth: Auth
) -> None:
    admin, student = await make_user('admin'), await make_user('student')
    headers = auth(student)
    assert (await client.get('/users/me', headers=headers)).status_code == 200

    await client.patch(f'/users/{student.id}', json={'is_active': False}, headers=auth(admin))

    assert (await client.get('/users/me', headers=headers)).status_code == 403

آزمون باگ کاربر غیرفعال: 02_solution/tests/test_capstone.py

۵. بازبینی کد

هر گروه پنج تا هفت دقیقه پروژه‌اش را ارائه می‌دهد و بعد به پرسش‌های گروه دیگر پاسخ می‌دهد. با کد خودتان روی صفحه پاسخ دهید و به فایل و خط اشاره کنید.

  1. مسیر یک درخواست، POST /courses/{id}/enrollments، را از روتر تا پایگاه داده و برگشت دنبال کنید. به کدام فایل‌ها دست می‌زند و چرا هر کدام جداست؟
  2. چرا برای یک دوره هم طرح‌واره Pydantic داریم و هم مدل SQLAlchemy؟ فیلدی نشان دهید که فقط در یکی از آنها هست.
  3. نگاشت‌های تایپ‌دار (Mapped[int]) در برابر سبک قدیمی Column(...) چه چیزی به ما می‌دهند؟
  4. پرس‌وجویی نشان دهید که از N+1 دوری می‌کند. چطور ثابتش می‌کنید؟
  5. چرا طرح پایگاه داده با Alembic تغییر می‌کند و نه با create_all؟
  6. سه جا را نشان دهید که تزریق وابستگی به کار رفته است. آزمون‌ها کدام را جایگزین کردند؟
  7. async کجا واقعاً به این API کمک می‌کند و کجا نه؟ اگر کسی در یک سرویس time.sleep() صدا بزند چه می‌شود؟
  8. جریان ورود را از فرم تا JWT توضیح دهید. چه چیزی در توکن هست و چه چیزی نیست؟
  9. API فرق ۴۰۱ و ۴۰۳ را از کجا می‌فهمد؟
  10. کدام قاعده جلوی مدرسی را می‌گیرد که می‌خواهد دوره مدرس دیگری را تغییر دهد؟
  11. فایل‌های بارگذاری‌شده چطور بررسی می‌شوند و کجا ذخیره می‌شوند؟
  12. آزمون‌هایتان چطور از API محافظت می‌کنند؟ یک آزمون شکست را نشان دهید که به آن افتخار می‌کنید.
  13. کلاینت پایتونی چطور با API صحبت می‌کند و وقتی سرور خاموش است چه می‌کند؟
  14. اگر یک روز دیگر وقت داشتید، اول چه چیزی را بهتر می‌کردید؟

۶. از اینجا به بعد

این دوره عمداً از چیزهایی مثل Redis، صف‌های کار، Kubernetes و استقرار ابری دوری کرد تا روی پایه‌ها تمرکز کند. قدم‌های منطقی بعدی:

فایل‌های تمرینی این فصل

پوشه S16-capstone شامل 01_starting_project (نقطه شروع شما، اول TASKS.md را بخوانید)، 02_solution (یک راه‌حل کامل) و code-review.md (پرسش‌های بازبینی) است.

۷. خلاصه دوره

فصلیک جمله
۱FastAPI همان پایتون پیشرفته است که با تمام قدرت به کار رفته.
۲مسیر، تابع، کد وضعیت؛ و روترها برای نظم.
۳به کلاینت اعتماد نکن: هر ورودی و خروجی یک طرح‌واره دارد.
۴وابستگی‌ها ساختار را ممکن می‌کنند؛ سرویس‌ها از HTTP خبر ندارند.
۵نوع پایتونی خودِ ستون پایگاه داده است.
۶پرس‌وجو را بخوان، تراکنش را بشناس، قید را تور ایمنی بدان.
۷رابطه راحت است؛ N+1 را ببین و درستش کن.
۸طرح پایگاه داده تاریخچه دارد؛ فهرست‌ها صفحه دارند.
۹رمز را هرگز ذخیره نکن؛ دو شکست ورود را یکسان پاسخ بده.
۱۰۴۰۱ یعنی «کیستی»، ۴۰۳ یعنی «اجازه نداری».
۱۱به نوع فایل اعلام‌شده اعتماد نکن؛ بایت‌ها را بخوان.
۱۲async فقط وقتی کمک می‌کند که هیچ چیز مسدود نکند.
۱۳آزمون شکست به اندازه آزمون موفقیت مهم است.
۱۴یک کلاینت خوب پیچیدگی HTTP را پشت متدهای تایپ‌دار پنهان می‌کند.
۱۵فقط درخواست‌های idempotent را دوباره امتحان کن.
۱۶کدی که می‌توانی از آن دفاع کنی، کدی است که می‌فهمی.

تمرین‌های این فصل

  1. همه وظیفه‌های A تا E فایل TASKS.md را انجام دهید.
  2. به GET /courses/{id}/materials صفحه‌بندی با Page[T] اضافه کنید.
  3. وضعیت completed را اضافه کنید: POST /courses/{id}/complete/{student_id} برای مدرس دوره و یک endpoint گواهی که فقط برای دانشجوی تمام‌کرده یک فایل متنی برگرداند.
  4. کل پروژه را روی PostgreSQL با asyncpg اجرا کنید و مجموعه Bruno را دوباره اجرا کنید.
  5. با DATABASE_ECHO=true تعداد دستورهای SQL GET /users/stats را پیش و پس از بازنویسی بشمارید.
  6. پروژه پایانی: API کتابخانه را به یک برنامه کامل با همین شکل تبدیل کنید: روترها، طرح‌واره‌ها، سرویس‌ها، وابستگی‌ها، تنظیمات، مدل‌ها با رابطه، مهاجرت‌های Alembic، JWT با دو نقش، بارگذاری جلد، صفحه‌بندی و فیلتر، دست‌کم سی آزمون، یک مجموعه Bruno و یک کلاینت ناهمگام با داشبورد. با پرسش‌های بازبینی این فصل از آن دفاع کنید.
فصل قبل: کلاینت ناهمگام، تلاش دوباره و یکپارچه‌سازی بازگشت به فهرست مطالب