Files
Diagramo_archive/sample.ipynb
2025-11-15 11:02:05 +03:30

3965 lines
214 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# مقدمه\n",
"\n",
"توی این استایل‌گاید، قراره با هم یاد بگیریم چطور پروژه‌های Django رو\n",
"طوری طراحی کنیم که **هم تمیزتر، هم مقیاس‌پذیرتر و هم لذت‌بخش‌تر** باشن.\n",
"\n",
"ایده‌ی اصلی اینه که بجای اینکه همه‌چیز رو توی View یا Model بنویسیم،\n",
"بیایم پروژه رو به چند **لایه‌ی منطقی (Logic Layers)** تقسیم کنیم،\n",
"که هرکدوم وظیفه‌ی خودش رو داره و کدها از هم **جدا و قابل تست** می‌شن.\n",
"\n",
"---\n",
"\n",
"## هدف نهایی\n",
"قراره به جایی برسیم که:\n",
"\n",
"- بدونیم هر بخش از منطق برنامه کجا باید نوشته بشه.\n",
"- بتونیم با خیال راحت تست بنویسیم و خطاها رو هندل کنیم.\n",
"- وظیفه‌ی هر لایه (Service, Selector, API, Serializer و ...) مشخص باشه.\n",
"- اگه یه بخشی از سیستم بزرگ شد، بتونیم راحت جداش کنیم و مدیریت کنیم.\n",
"- کدها هم برای خودمون، هم برای هم‌تیمی‌هامون **خوانا، امن و توسعه‌پذیر** باشن.\n",
"\n",
"---\n",
"\n",
"## ساختار کلی این راهنما\n",
"\n",
"در ادامه قراره با این مباحث آشنا بشیم:\n",
"\n",
"1. **API Layer** نقطه‌ی ورود درخواست‌ها، مسئول دریافت و پاسخ HTTP.\n",
"2. **Service Layer** مسئول منطق اصلی کسب‌وکار (Business Logic).\n",
"3. **Selector Layer** لایه‌ی مخصوص خواندن داده از دیتابیس.\n",
"4. **Serializer Layer** آماده‌سازی داده‌ها برای API، با prefetch و caching هوشمند.\n",
"5. **URLs Structure** سازماندهی مسیرها به روش قابل نگهداری در پروژه‌های بزرگ.\n",
"6. **Settings Structure** تقسیم تنظیمات Django و ۳rd partyها برای محیط‌های مختلف.\n",
"7. **Error Handling** چطور خطاها رو یک‌دست و قابل پیش‌بینی مدیریت کنیم.\n",
"8. **Celery Integration** اجرای کارهای پس‌زمینه و Async به روش تمیز.\n",
"9. **Testing Strategy** چطور هر لایه رو جداگانه و مطمئن تست کنیم.\n",
"\n",
"---\n",
"\n",
"در نهایت، هدف ما اینه که:\n",
"> «کدی بنویسیم که هم زیبا باشه، هم قابل فهم، هم انعطاف‌پذیر —\n",
"> و موقع خوندنش حس نکنیم تو جهنم افتادیم »\n"
],
"id": "2f68eed541fac7be"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Django Styleguide — Explained My Way\n",
"\n",
"این خلاصه‌ایه از **Django Styleguide** نوشته‌ی HackSoft — ولی به زبون خودمونی‌تر.\n",
"\n",
"اینجا با نتیجه‌ی سال‌ها پروژه‌های کوچیک و بزرگ طرفی و سه تا راه داری برای برخورد باهاش:\n",
"\n",
"1. مو به مو ازش پیروی کن.\n",
"2. هرچی برات معنی‌دار بود انتخاب کن .\n",
"3. یا کلاً بی‌خیالش شو — ولی اگه گزینه‌ی سه‌ای… چرا اصلاً اینجایی؟\n",
"\n",
"---\n",
"\n",
"## ایده‌ی اصلی\n",
"\n",
"هرچی ابزار و امکانات پروژه بیشتر بشن، نیازت به ظرف‌های منظم‌تر هم بیشتر می‌شه.\n",
"هرچی پروژه بزرگ‌تر، لایه‌ها هم باید واضح‌تر باشن.\n",
"\n",
"تو Django منطق پروژه (Business Logic) باید داخل اینا باشه:\n",
"\n",
"- **Services** → توابعی که بیشتر کار نوشتن توی دیتابیس رو انجام میدن.\n",
"- **Selectors** → توابعی که بیشتر کار خوندن از دیتابیس رو می‌کنن.\n",
"- **Model Properties** (در موارد خاص).\n",
"- **متد `clean()`** برای اعتبارسنجی اضافه (در موارد خاص).\n",
"\n",
"و نباید داخل اینا باشه:\n",
"\n",
"- APIها و Viewها\n",
"- Serializerها و Formها\n",
"- تگ‌های Form\n",
"- متد `save()` مدل\n",
"- Custom Managerها یا QuerySetها\n",
"- Signalها\n",
"\n",
"---\n",
"\n",
"## Model Property یا Selector؟\n",
"\n",
"- اگه Property مربوط به چند رابطه (multi-relation) باشه → ببرش تو Selector.\n",
"- اگه Property پیچیده‌ست و ممکنه باعث **N + 1 query problem** بشه → باز هم Selector.\n",
"\n",
"هدف کلی:\n",
"**\"Separate Concerns\"** — یعنی هر بخش فقط کار خودش رو انجام بده تا بشه راحت‌تر نگهش داشت و تستش کرد.\n",
"\n",
"---\n",
"\n",
"## 🤔 چرا نه View یا Serializer؟\n",
"\n",
"چون وقتی منطق رو پخش کنی بین View و Serializer و Form،\n",
"کد تیکه‌تیکه می‌شه، جریان دیتا گم می‌شه،\n",
"و برای یه تغییر ساده باید بری دل abstractionها ببینی چی به چیه.\n",
"\n",
"بله، generic View و serializerها برای CRUD ساده عالی‌ان،\n",
"ولی وقتی از مسیر خوش و خرم CRUD خارج شدی، وارد باس‌فایت می‌شی\n",
"اون‌جاست که اوضاع شلخته می‌شه و کم‌کم یه **لگسی ریپو** ازت جا می‌مونه.\n",
"\n",
"پس این Styleguide اومده تا برات **جعبه‌** تعریف کنه:\n",
"تا بتونی منطق رو توی جای درستش بچینی، برای پروژه‌ی خودت.\n",
"\n",
"---\n",
"\n",
"## Core در برابر Interface\n",
"\n",
"رفتار اصلی برنامه (business logic) نباید قاطی لایه‌ی ارتباطش با بیرون بشه.\n",
"API، CLI، management command — اینا فقط Interface ان.\n",
"مرز بین **core** و **interface** باید واضح باشه.\n",
"بعضی وقتا قاطی می‌شن، ولی داشتن این مرز یعنی پروژه‌ات تمیز، تست‌پذیر و قابل‌گسترشه.\n",
"\n",
"---\n",
"\n",
"## چرا نه Custom Manager یا QuerySet؟\n",
"\n",
"ایده‌ی بدی نیست، اما کافی نیست.\n",
"\n",
"ممکنه Manager/QuerySet بهتر برای مدل بسازی،\n",
"ولی نمی‌تونی کل منطق پروژه رو اونجا بچپونی، چون:\n",
"\n",
"- منطق پروژه همیشه دقیقاً با مدل یکی نیست.\n",
"- معمولاً بین چند تا مدل پخش می‌شه (A و B و C و D و …).\n",
"- بعضی وقتا باید با سرویس‌های خارجی کار کنه — و اونجا جاش توی Manager نیست.\n",
"\n",
"پس منطق رو از لایه‌ی دیتا جدا کن و بذار توی **Service Layer**.\n",
"\n",
"Service ممکنه تابع، کلاس، یا حتی ماژول باشه — هرچی منطقی‌تره برای مورد تو.\n",
"\n",
"Manager و QuerySet ابزارهای قدرتمندن،\n",
"ولی کارشون فقط ارائه‌ی **interface بهتر** برای مدل‌هاست، نه جای منطق اصلی.\n",
"\n",
"---\n",
"\n",
"## ⚠️ چرا نه Signal؟\n",
"\n",
"Signalها ابزار خوبی‌ان برای **چیزایی که نباید همدیگه رو بشناسن**\n",
"ولی می‌خوای وصلشون کنی — مثلاً برای invalidate کردن cache.\n",
"\n",
"اما اگه منطق اصلی رو ببری اونجا،\n",
"اتصال‌ها **پنهون و غیرقابل‌ردیابی** می‌شن.\n",
"در نتیجه، دیباگش به جهنم ختم می‌شه 🔥\n",
"\n",
"بنابراین Signal فقط برای موارد خاص،\n",
"نه برای ساختار دادن به لایه‌ی domain/business.\n",
"\n",
"---\n",
"\n",
"## خلاصه‌ی داستان\n",
"\n",
"هرچی پروژه بزرگ‌تر، ساختار باید حرفه‌ای‌تر.\n",
"منطق رو بذار توی Service و Selector، نه توی View و Serializer.\n",
"Core رو از Interface جدا کن.\n",
"و لطفاً، لطفاً Signal رو فقط وقتی استفاده کن که واقعاً لازمه. 😅\n"
],
"id": "a0d4b30c7f3312b9"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# 🍪 Cookiecutter — پروژه رو با ساختار درست شروع کن\n",
"\n",
"اوه، در ضمن...\n",
"بیشتر دردسرهای اول پروژه معمولاً سر **فولدر‌بندی و فایل‌منیجمنت** شروع می‌شن\n",
"ولی خدا رو شکر یه چیز آماده هست به اسم **Cookiecutter** که کارت رو راحت می‌کنه.\n",
"\n",
"---\n",
"\n",
"## Cookiecutter چیه؟\n",
"\n",
"یه ابزار برای ساختن **اسکلت اولیه پروژه** ـه.\n",
"به جای اینکه هر بار بری دستی فولدر بسازی و `settings.py` و `config` و `tests` رو از صفر درست کنی،\n",
"با یه دستور ساده برات یه ساختار حرفه‌ای و آماده می‌سازه.\n",
"\n",
"مثلاً:\n",
"\n",
"```bash\n",
"cookiecutter https://github.com/cookiecutter/cookiecutter-django\n"
],
"id": "1d42ac476244445c"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Model\n",
"\n",
"مدل فقط باید مسئول **داده (Data Model)** باشه — نه چیز دیگه.\n",
"یعنی کاری به منطق (Business Logic)، API یا سایر لایه‌ها نداره.\n",
"مدل فقط باید تعریف کنه **چی ذخیره می‌کنیم** و **چطور ذخیره می‌کنیم**.\n",
"\n",
"---\n",
"\n",
"## BaseModel\n",
"\n",
"ما معمولاً از **تکرار کد** خوشمون نمیاد (شما هم احتمالاً همین‌طور ).\n",
"پس بهترین کار اینه که برای همه‌ی مدل‌ها **یه مدل پایه (Parent Model)** تعریف کنیم.\n",
"\n",
"این مدل پایه، فیلدهایی رو نگه می‌داره که تقریباً همه‌ی مدل‌ها بهش نیاز دارن.\n",
"معمول‌ترینش هم ایناست:\n",
"\n",
"- `created_at` → تاریخ ساخت\n",
"- `updated_at` → تاریخ آخرین تغییر\n",
"\n",
"به این ترتیب لازم نیست توی هر مدل جداگانه این فیلدها رو تکرار کنیم.\n",
"کد تمیزتر، کوتاه‌تر و قابل‌نگهداری‌تر می‌مونه.\n",
"\n"
],
"id": "f45895968b2f2150"
},
{
"metadata": {},
"cell_type": "code",
"source": [
"from django.db import models\n",
"from django.utils import timezone\n",
"\n",
"\n",
"class BaseModel(models.Model):\n",
" created_at = models.DateTimeField(db_index=True, default=timezone.now)\n",
" updated_at = models.DateTimeField(auto_now=True)\n",
"\n",
" class Meta:\n",
" abstract = True"
],
"id": "863c92fa490a623",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"cell_type": "markdown",
"source": " هر موقع هم مودل جدید خواستی فقط از `BaseModel` ارث بری کن",
"id": "b4ba7889997addfb"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class SomeModel(BaseModel):\n",
" pass"
],
"id": "57747f8d45cf2c67"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# ✅ Validation — `clean()` & `full_clean()`\n",
" گاهی نیازه که در مودل خود مطمعن بشیم که دیتایی که در حال ارسال ذخیره سازی هست درسته یا نه"
],
"id": "c35ef5d471064d6a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class Course(BaseModel):\n",
" name = models.CharField(unique=True, max_length=255)\n",
" start_date = models.DateField()\n",
" end_date = models.DateField()\n",
"\n",
" def clean(self):\n",
" if self.start_date >= self.end_date:\n",
" raise ValidationError(\"End date cannot be before start date\")\n"
],
"id": "7f2183666c9579ee"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"اینجا با متد `clean()` داریم مطمئن می‌شیم که داده‌ی اشتباه وارد دیتابیس نشه.\n",
"اما دقت کن! برای اینکه `clean()` صدا زده بشه باید قبل از `save()`،\n",
"یه بار `full_clean()` روی شیء اجرا بشه 👇"
],
"id": "2a40301792435642"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def course_create(*, name: str, start_date: date, end_date: date) -> Course:\n",
" obj = Course(name=name, start_date=start_date, end_date=end_date)\n",
"\n",
" obj.full_clean() # اعتبارسنجی\n",
" obj.save() # ذخیره در دیتابیس\n",
"\n",
" return obj\n"
],
"id": "8e9df84a2740ada5"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"این روش هم با Django Admin هماهنگه،\n",
"چون اونجا فرم‌ها خودشون `full_clean()` رو صدا می‌زنن.\n",
"\n",
"---\n",
"\n",
"## 📋 قانون کلی\n",
"\n",
"✅ وقتی اعتبارسنجی فقط روی چند تا **فیلد ساده‌ی مدل** هست → توی `clean()` بنویس.\n",
"⚙️ وقتی اعتبارسنجی پیچیده‌ست یا نیاز به **داده از مدل‌های دیگه** داری → ببرش توی **service layer**.\n"
],
"id": "4349f0b2b2e08b97"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Validation — Constraints\n",
"\n",
"اگر بشه با Constraint انجامش داد، همیشه اون راه بهتره 👇\n",
"چون دیتابیس خودش جلوی داده‌ی اشتباه رو می‌گیره (حتی از بیرون Django).\n"
],
"id": "8a6e4f232c7ae222"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class Course(BaseModel):\n",
" name = models.CharField(unique=True, max_length=255)\n",
" start_date = models.DateField()\n",
" end_date = models.DateField()\n",
"\n",
" class Meta:\n",
" constraints = [\n",
" models.CheckConstraint(\n",
" name=\"start_date_before_end_date\",\n",
" check=Q(start_date__lt=F(\"end_date\"))\n",
" )\n",
" ]\n"
],
"id": "ff2dab1ff2abd89"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"از Django 4.1 به بعد،\n",
"`full_clean()` حتی **constraints**ها رو هم چک می‌کنه 👀\n",
"پس به جای `IntegrityError`، `ValidationError` می‌گیری.\n",
"\n",
"📚 [Docs](https://docs.djangoproject.com/en/4.1/ref/models/instances/#validating-objects)\n"
],
"id": "da2002f4149ecdc7"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Properties\n",
"\n",
"گاهی لازمه مقدارهای ساده‌ی مشتق‌شده از فیلدها رو سریع بگیریم.\n",
"اونجا بهترین گزینه، `@property` هست.\n"
],
"id": "c28b41382d9cc6f3"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.utils import timezone\n",
"\n",
"class Course(BaseModel):\n",
" ...\n",
" @property\n",
" def has_started(self) -> bool:\n",
" now = timezone.now()\n",
" return self.start_date <= now.date()\n",
"\n",
" @property\n",
" def has_finished(self) -> bool:\n",
" now = timezone.now()\n",
" return self.end_date <= now.date()\n"
],
"id": "46be994e2cb5c266"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"الان می‌تونی توی serializer یا template بنویسی:\n",
"\n",
"```python\n",
"course.has_started\n",
"course.has_finished\n"
],
"id": "8237eece2f794240"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Methods\n",
"\n",
"متدهای مدل ابزار قدرتمندی هستن که می‌تونن روی properties ساخته بشن یا داده‌های مدل رو تغییر بدن.\n",
"\n",
"---\n",
"\n",
"## 🧩 مثال ساده\n",
"\n",
"در این مثال، متد `is_within(self, x)` بررسی می‌کنه که آیا تاریخ `x` بین `start_date` و `end_date` هست یا نه.\n"
],
"id": "464c1da129b3874"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import ValidationError\n",
"from django.utils import timezone\n",
"from datetime import date\n",
"\n",
"\n",
"class Course(BaseModel):\n",
" name = models.CharField(unique=True, max_length=255)\n",
" start_date = models.DateField()\n",
" end_date = models.DateField()\n",
"\n",
" def clean(self):\n",
" if self.start_date >= self.end_date:\n",
" raise ValidationError(\"End date cannot be before start date\")\n",
"\n",
" @property\n",
" def has_started(self) -> bool:\n",
" now = timezone.now()\n",
" return self.start_date <= now.date()\n",
"\n",
" @property\n",
" def has_finished(self) -> bool:\n",
" now = timezone.now()\n",
" return self.end_date <= now.date()\n",
"\n",
" def is_within(self, x: date) -> bool:\n",
" return self.start_date <= x <= self.end_date\n"
],
"id": "9f0b439a9f9d0884"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"متد `is_within` نمی‌تونه property باشه چون نیاز به آرگومان داره، پس به‌صورت متد تعریف میشه.\n",
"\n",
"---\n",
"\n",
"## 🔁 مثال دوم: ست‌کردن چند مقدار با هم\n",
"\n",
"گاهی وقتا وقتی یک attribute تغییر می‌کنه، باید attribute دیگه‌ای هم بر اساس اون آپدیت بشه.\n",
"در این مواقع می‌تونیم از یک متد استفاده کنیم.\n",
"اینجا چون `x` ورودی داره، دیگه property نیست، method حساب میشه.\n"
],
"id": "87ee9f1eb34fa889"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.utils.crypto import get_random_string\n",
"from django.conf import settings\n",
"from django.utils import timezone\n",
"\n",
"\n",
"class Token(BaseModel):\n",
" secret = models.CharField(max_length=255, unique=True)\n",
" expiry = models.DateTimeField(blank=True, null=True)\n",
"\n",
" def set_new_secret(self):\n",
" now = timezone.now()\n",
" self.secret = get_random_string(255)\n",
" self.expiry = now + settings.TOKEN_EXPIRY_TIMEDELTA\n",
" return self\n"
],
"id": "f3f24d65fb578fc2"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"با صدا زدن `set_new_secret()`، هر دو مقدار `secret` و `expiry` به‌درستی مقداردهی می‌شن.\n",
"\n",
"---\n",
"\n",
"## 📜 قانون کلی\n",
"\n",
"✅ اگه مقدار جدید فقط از خود فیلدهای مدل حساب میشه → متد.\n",
"⚙️ اگه باید از مدل‌های دیگه داده بگیری یا محاسبه پیچیده‌ست → سرویس (Service).\n",
"\n",
"---\n",
"\n",
"# 🧪 Testing\n",
"\n",
"مدل‌ها فقط وقتی تست نیاز دارن که منطق خاصی داشته باشن (مثل validation یا property یا method).\n"
],
"id": "6ea8b2ecd4ec994b"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from datetime import timedelta\n",
"from django.test import TestCase\n",
"from django.core.exceptions import ValidationError\n",
"from django.utils import timezone\n",
"from project.some_app.models import Course\n",
"\n",
"\n",
"class CourseTests(TestCase):\n",
" def test_course_end_date_cannot_be_before_start_date(self):\n",
" start_date = timezone.now()\n",
" end_date = timezone.now() - timedelta(days=1)\n",
"\n",
" course = Course(start_date=start_date, end_date=end_date)\n",
"\n",
" with self.assertRaises(ValidationError):\n",
" course.full_clean()\n"
],
"id": "592b86eae5ab75f8"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"✅ نکات مهم:\n",
"- `full_clean()` بررسی می‌کنه که اعتبار داده‌ها درست باشه.\n",
"- نیازی نیست به دیتابیس وصل بشیم، چون فقط منطق مدل تست میشه → تست سریع‌تر میشه.\n"
],
"id": "d7153c4801ac18f9"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Services\n",
"\n",
"سرویس‌ها جایی هستن که **منطق تجاری (Business Logic)** پروژه در اون‌ها قرار می‌گیره.\n",
"یعنی هر چیزی که به قوانین یا رفتار اصلی سیستم مربوطه — نه صرفاً ذخیره‌سازی یا نمایش داده.\n",
"\n",
"لایه‌ی Service زبانی متناسب با **دامنه‌ی پروژه (Domain Language)** صحبت می‌کنه،\n",
"به دیتابیس و منابع خارجی دسترسی داره،\n",
"و می‌تونه با بخش‌های دیگه‌ی سیستم تعامل کنه.\n",
"\n",
"## جایگاه در ساختار پروژه\n",
"\n",
"لایه‌ی Service معمولاً بین **View** و **Model** قرار می‌گیره:\n",
"\n",
"[ Views ] → [ Services ] → [ Models ]"
],
"id": "47bfa5655b35cfcd"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "![image.png](attachment:a56fbb8b-78e7-4442-bb46-e3974577e458.png)",
"id": "810e866eefd4da64",
"attachments": {
"a56fbb8b-78e7-4442-bb46-e3974577e458.png": {
"image/png": "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"
}
}
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"## ساختار کلی Service\n",
"\n",
"یک سرویس می‌تونه هرکدوم از اینا باشه:\n",
"\n",
"- یک تابع ساده\n",
"- یک کلاس\n",
"- حتی یک ماژول کامل\n",
"- یا هر چیزی که **برای پروژه‌ت منطقی باشه**\n",
"\n",
"\n",
"## معمول‌ترین حالت\n",
"\n",
"در بیشتر موارد، سرویس‌ها فقط **توابع ساده** هستن که:\n",
"\n",
"- داخل فایل `<your_app>/services.py` قرار می‌گیرن\n",
"- ورودی‌هاشون **keyword-only** هست (مگر اینکه فقط یه آرگومان نیاز داشته باشن)\n",
"- با **type hint** نوشته می‌شن، حتی اگه فعلاً از `mypy` استفاده نکنی\n",
"- با دیتابیس، فایل‌ها، APIها یا سرویس‌های دیگه ارتباط دارن\n",
"- منطق اصلی بیزینس رو انجام می‌دن — از ساخت ساده‌ی مدل‌ها گرفته تا عملیات پیچیده یا فراخوانی سرویس‌های خارجی\n",
"\n",
"---\n",
"\n",
"Example - function-based service"
],
"id": "7217107a64af0d96"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def user_create(\n",
" *,\n",
" email: str,\n",
" name: str\n",
") -> User:\n",
" user = User(email=email)\n",
" user.full_clean()\n",
" user.save()\n",
"\n",
" profile_create(user=user, name=name)\n",
" confirmation_email_send(user=user)\n",
"\n",
" return user"
],
"id": "b6bd9661823ed54c"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "Example - class-based service\n",
"id": "8975bbd5ce5df8a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py\n",
"\n",
"\n",
"class FileStandardUploadService:\n",
" \"\"\"\n",
" This also serves as an example of a service class,\n",
" which encapsulates 2 different behaviors (create & update) under a namespace.\n",
"\n",
" Meaning, we use the class here for:\n",
"\n",
" 1. The namespace\n",
" 2. The ability to reuse `_infer_file_name_and_type` (which can also be an util)\n",
" \"\"\"\n",
" def __init__(self, user: BaseUser, file_obj):\n",
" self.user = user\n",
" self.file_obj = file_obj\n",
"\n",
" def _infer_file_name_and_type(self, file_name: str = \"\", file_type: str = \"\") -> Tuple[str, str]:\n",
" file_name = file_name or self.file_obj.name\n",
"\n",
" if not file_type:\n",
" guessed_file_type, encoding = mimetypes.guess_type(file_name)\n",
" file_type = guessed_file_type or \"\"\n",
"\n",
" return file_name, file_type\n",
"\n",
" @transaction.atomic\n",
" def create(self, file_name: str = \"\", file_type: str = \"\") -> File:\n",
" _validate_file_size(self.file_obj)\n",
"\n",
" file_name, file_type = self._infer_file_name_and_type(file_name, file_type)\n",
"\n",
" obj = File(\n",
" file=self.file_obj,\n",
" original_file_name=file_name,\n",
" file_name=file_generate_name(file_name),\n",
" file_type=file_type,\n",
" uploaded_by=self.user,\n",
" upload_finished_at=timezone.now()\n",
" )\n",
"\n",
" obj.full_clean()\n",
" obj.save()\n",
"\n",
" return obj\n",
"\n",
" @transaction.atomic\n",
" def update(self, file: File, file_name: str = \"\", file_type: str = \"\") -> File:\n",
" _validate_file_size(self.file_obj)\n",
"\n",
" file_name, file_type = self._infer_file_name_and_type(file_name, file_type)\n",
"\n",
" file.file = self.file_obj\n",
" file.original_file_name = file_name\n",
" file.file_name = file_generate_name(file_name)\n",
" file.file_type = file_type\n",
" file.uploaded_by = self.user\n",
" file.upload_finished_at = timezone.now()\n",
"\n",
" file.full_clean()\n",
" file.save()\n",
"\n",
" return file"
],
"id": "c8e3aaa27bfb4ee3"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"\n",
"\n",
"همونطور که تو کامنت گفته شد، ما این روش رو به دو دلیل اصلی استفاده می‌کنیم:\n",
"\n",
"1. **Namespace**\n",
" - یک فضای نام واحد داریم برای `create` و `update`.\n",
"\n",
"2. **Reuse**\n",
" - می‌خوایم از منطق `_infer_file_name_and_type` دوباره استفاده کنیم.\n",
"\n",
"در ادامه، نمونه‌ای از نحوه‌ی استفاده‌ی این سرویس رو می‌بینیم:\n"
],
"id": "cecbfe320f4741ae"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/apis.py\n",
"\n",
"class FileDirectUploadApi(ApiAuthMixin, APIView):\n",
" def post(self, request):\n",
" service = FileDirectUploadService(\n",
" user=request.user,\n",
" file_obj=request.FILES[\"file\"]\n",
" )\n",
" file = service.create()\n",
"\n",
" return Response(data={\"id\": file.id}, status=status.HTTP_201_CREATED)"
],
"id": "e067d685d21197af"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"@admin.register(File)\n",
"class FileAdmin(admin.ModelAdmin):\n",
" # ... other code here ...\n",
" # https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/admin.py\n",
"\n",
" def save_model(self, request, obj, form, change):\n",
" try:\n",
" cleaned_data = form.cleaned_data\n",
"\n",
" service = FileDirectUploadService(\n",
" file_obj=cleaned_data[\"file\"],\n",
" user=cleaned_data[\"uploaded_by\"]\n",
" )\n",
"\n",
" if change:\n",
" service.update(file=obj)\n",
" else:\n",
" service.create()\n",
" except ValidationError as exc:\n",
" self.message_user(request, str(exc), messages.ERROR)"
],
"id": "52d5d1cf07308729"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " استفاده از classbased خیلی برای کار ها دارای جریان مفیده مثل کار های مولتی استیج",
"id": "47a806c2ff87b03a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py\n",
"\n",
"\n",
"class FileDirectUploadService:\n",
" \"\"\"\n",
" This also serves as an example of a service class,\n",
" which encapsulates a flow (start & finish) + one-off action (upload_local) into a namespace.\n",
"\n",
" Meaning, we use the class here for:\n",
"\n",
" 1. The namespace\n",
" \"\"\"\n",
" def __init__(self, user: BaseUser):\n",
" self.user = user\n",
"\n",
" @transaction.atomic\n",
" def start(self, *, file_name: str, file_type: str) -> Dict[str, Any]:\n",
" file = File(\n",
" original_file_name=file_name,\n",
" file_name=file_generate_name(file_name),\n",
" file_type=file_type,\n",
" uploaded_by=self.user,\n",
" file=None\n",
" )\n",
" file.full_clean()\n",
" file.save()\n",
"\n",
" upload_path = file_generate_upload_path(file, file.file_name)\n",
"\n",
" \"\"\"\n",
" We are doing this in order to have an associated file for the field.\n",
" \"\"\"\n",
" file.file = file.file.field.attr_class(file, file.file.field, upload_path)\n",
" file.save()\n",
"\n",
" presigned_data: Dict[str, Any] = {}\n",
"\n",
" if settings.FILE_UPLOAD_STORAGE == FileUploadStorage.S3:\n",
" presigned_data = s3_generate_presigned_post(\n",
" file_path=upload_path, file_type=file.file_type\n",
" )\n",
"\n",
" else:\n",
" presigned_data = {\n",
" \"url\": file_generate_local_upload_url(file_id=str(file.id)),\n",
" }\n",
"\n",
" return {\"id\": file.id, **presigned_data}\n",
"\n",
" @transaction.atomic\n",
" def finish(self, *, file: File) -> File:\n",
" # Potentially, check against user\n",
" file.upload_finished_at = timezone.now()\n",
" file.full_clean()\n",
" file.save()\n",
"\n",
" return file"
],
"id": "f44e1ac4b30189b1"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"\n",
"نام‌گذاری سرویس‌ها به سلیقه‌ی شما بستگی داره، اما مهمه که **یکدست و قابل پیش‌بینی** باشه.\n",
"\n",
"اگر به مثال قبلی نگاه کنیم، سرویس ما `user_create` نام داره.\n",
"الگوی پیشنهادی: `<entity>_<action>`.\n",
"\n",
"1. **Namespace**\n",
" - راحت می‌تونیم همه‌ی سرویس‌هایی که با `user_` شروع می‌شن رو پیدا کنیم.\n",
" - همچنین می‌تونیم اون‌ها رو در یک ماژول جداگانه مثل `users.py` قرار بدیم.\n",
"\n",
"2. **Greppability**\n",
" - اگر بخوایم همه‌ی عملیات مرتبط با یک موجودیت خاص رو ببینیم، کافیه grep کنیم روی `user_`."
],
"id": "f9066ce8d21bd233"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Modules\n",
"\n",
"اگه یک اپ ساده‌ی Django داری و تعداد کمی سرویس داری، می‌تونی همه رو داخل یک فایل `services.py` نگه داری.\n",
"\n",
"---\n",
"\n",
"## وقتی پروژه بزرگ میشه\n",
"\n",
"وقتی سرویس‌ها زیاد شدن، بهتره `services.py` رو به یک **پکیج (فولدر) با زیرماژول‌ها** تبدیل کنی،\n",
"بسته به **ساب‌دامین‌ها** یا بخش‌های مختلفی که در اپ داری.\n",
"\n",
"---\n",
"\n",
"فرض کنیم یک اپ Authentication داریم:\n",
"\n",
"- یک زیرماژول برای **JWT**\n",
"- یک زیرماژول برای **OAuth**\n",
"\n",
"```text\n",
"services\n",
"├── __init__.py\n",
"├── jwt.py\n",
"└── oauth.py"
],
"id": "bf2e2bda65226330"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Flavors of Service Modules\n",
"\n",
"چند روش مختلف برای ساختاردهی سرویس‌ها وجود داره:\n",
"\n",
"1. **Import-export dance**\n",
" - می‌تونی داخل `services/__init__.py` همه‌ی سرویس‌ها رو ایمپورت کنی،\n",
" - تا بتونی از هر جای پروژه، فقط با `from project.authentication.services import ...` دسترسی داشته باشی.\n",
"\n",
"2. **Folder-module**\n",
" - می‌تونی یک فولدر مثل `jwt/` بسازی و `__init__.py` داشته باشه،\n",
" - و تمام کدهای مرتبط با JWT رو اونجا بذاری.\n",
"\n",
"3. **Flexibility**\n",
" - در نهایت، ساختار کاملاً به خودت بستگی داره.\n",
" - اگه حس کردی وقتشه بازسازی و ریفکتور انجام بدی — همین کار رو بکن"
],
"id": "f43a82fe12fd62c0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Selectors\n",
"\n",
"در بیشتر پروژه‌هامون، بین **پوش دادن (Push)** و **پول کشیدن (Pull)** داده‌ها از دیتابیس تفاوت قائل می‌شیم:\n",
"\n",
"- 🧩 **Services** مسئول \"Push\" هستن — یعنی وارد کردن، تغییر دادن یا حذف داده‌ها در دیتابیس.\n",
"- 🔍 **Selectors** مسئول \"Pull\" هستن — یعنی واکشی و خواندن داده‌ها از دیتابیس.\n",
"\n",
"به عبارتی، **Selectors** یک «زیرلایه» از سرویس‌ها هستن که تخصصش در گرفتن داده‌هاست.\n",
"\n",
"اگه با این ایده خیلی ارتباط نمی‌گیری، هیچ اشکالی نداره — می‌تونی برای هر دو نوع عملیات فقط از سرویس‌ها استفاده کنی.\n",
"\n",
"---\n",
"\n",
"## قواعد Selector\n",
"\n",
"یک Selector دقیقاً از همان قوانین Service پیروی می‌کند:\n",
"\n",
"- می‌تونه تابع، کلاس یا ماژول باشه.\n",
"- در `<your_app>/selectors.py` قرار می‌گیره.\n",
"- ورودی‌هاش باید شفاف و keyword-only باشن.\n",
"- خروجی معمولاً queryset یا object برمی‌گردونه.\n",
"\n",
"---"
],
"id": "14fffe05635a6b5a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def user_list(*, fetched_by: User) -> Iterable[User]:\n",
" user_ids = user_get_visible_for(user=fetched_by)\n",
"\n",
" query = Q(id__in=user_ids)\n",
"\n",
" return User.objects.filter(query)"
],
"id": "11a6a260e9e93772"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Testing\n",
"\n",
"از اون‌جایی که **Service Layer** منطق اصلی کسب‌وکار (Business Logic) رو در خودش داره،\n",
"بهترین نقطه برای نوشتن تست‌های دقیق و هدفمند هم هست.\n",
"\n",
"---\n",
"\n",
"## قوانین کلی برای تست سرویس‌ها\n",
"\n",
"اگر تصمیم گرفتی **service layer** رو با تست پوشش بدی، این چند قانون کلی رو در نظر بگیر:\n",
"\n",
"1. **تست‌ها باید تمام منطق کسب‌وکار رو به‌صورت کامل پوشش بدن.**\n",
" یعنی تمام مسیرهای ممکن در تابع یا کلاس تست بشن.\n",
"\n",
"2. **تست‌ها باید به دیتابیس واقعی (در محیط تست) دسترسی داشته باشن.**\n",
" یعنی داده بسازن، ذخیره کنن و دوباره بخونن.\n",
"\n",
"3. **هر چیزی که از پروژه خارج می‌شه باید Mock بشه.**\n",
" مثل:\n",
" - فراخوانی async taskها\n",
" - درخواست به APIهای خارجی\n",
" - ایمیل‌زدن یا پیام‌فرستادن\n",
"\n",
"---\n",
"\n",
"## ساخت وضعیت مورد نیاز برای تست\n",
"\n",
"برای ساخت حالت اولیه (state) مورد نیاز هر تست، می‌تونی از ترکیب روش‌های زیر استفاده کنی:\n",
"\n",
"- **Fakes** → (مثل کتابخانه `faker`) برای ساخت داده‌ی تصادفی و طبیعی.\n",
"- **سرویس‌های دیگر** → از سرویس‌های موجود برای ساخت آبجکت‌های مورد نیاز استفاده کن.\n",
"- **Test utilities & helpers** → متدهای کمکی مخصوص تست‌ها.\n",
"- **Factories** → پیشنهاد می‌کنیم از `factory_boy` استفاده کنی.\n",
"- **Plain `.objects.create()` calls** → اگر هنوز factory نداری، مستقیم با ORM بساز.\n",
"\n",
"---\n",
"\n",
"در نهایت، هر روشی که بهت کمک کنه تست‌هایت تمیز، خوانا و پایدار باشن، بهترین انتخابه.\n"
],
"id": "46c62fdd47f9f14e"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.contrib.auth.models import User\n",
"from django.core.exceptions import ValidationError\n",
"from django.db import transaction\n",
"\n",
"from project.payments.selectors import items_get_for_user\n",
"from project.payments.models import Item, Payment\n",
"from project.payments.tasks import payment_charge\n",
"\n",
"\n",
"@transaction.atomic\n",
"def item_buy(\n",
" *,\n",
" item: Item,\n",
" user: User,\n",
") -> Payment:\n",
" if item in items_get_for_user(user=user):\n",
" raise ValidationError(f'Item {item} already in {user} items.')\n",
"\n",
" payment = Payment(\n",
" item=item,\n",
" user=user,\n",
" successful=False\n",
" )\n",
" payment.full_clean()\n",
" payment.save()\n",
"\n",
" # Run the task once the transaction has commited,\n",
" # guaranteeing the object has been created.\n",
" transaction.on_commit(\n",
" lambda: payment_charge.delay(payment_id=payment.id)\n",
" )\n",
"\n",
" return payment"
],
"id": "cafdccd628863788"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"1. **فراخوانی یک Selector برای اعتبارسنجی (Validation)**\n",
" ابتدا داده‌ها رو از دیتابیس واکشی می‌کنه یا بررسی می‌کنه.\n",
"\n",
"2. **ایجاد یک آبجکت**\n",
" بعد از اعتبارسنجی، یک آبجکت جدید ایجاد می‌کنه.\n",
"\n",
"3. **تاخیر یک تسک**\n",
" در نهایت، یک تسک async رو به صورت delayed فراخوانی می‌کنه برای انجام کارهای پس‌زمینه."
],
"id": "e516af97cdcd05aa"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from unittest.mock import patch, Mock\n",
"\n",
"from django.test import TestCase\n",
"from django.contrib.auth.models import User\n",
"from django.core.exceptions import ValidationError\n",
"\n",
"from django_styleguide.payments.services import item_buy\n",
"from django_styleguide.payments.models import Payment, Item\n",
"\n",
"\n",
"class ItemBuyTests(TestCase):\n",
" @patch('project.payments.services.items_get_for_user')\n",
" def test_buying_item_that_is_already_bought_fails(\n",
" self, items_get_for_user_mock: Mock\n",
" ):\n",
" \"\"\"\n",
" Since we already have tests for `items_get_for_user`,\n",
" we can safely mock it here and give it a proper return value.\n",
" \"\"\"\n",
" user = User(username='Test User')\n",
" item = Item(\n",
" name='Test Item',\n",
" description='Test Item description',\n",
" price=10.15\n",
" )\n",
"\n",
" items_get_for_user_mock.return_value = [item]\n",
"\n",
" with self.assertRaises(ValidationError):\n",
" item_buy(user=user, item=item)\n",
"\n",
" @patch('project.payments.services.payment_charge.delay')\n",
" def test_buying_item_creates_a_payment_and_calls_charge_task(\n",
" self,\n",
" payment_charge_mock: Mock\n",
" ):\n",
" # How we prepare our tests is a topic for a different discussion\n",
" user = given_a_user(username=\"Test user\")\n",
" item = given_a_item(\n",
" name='Test Item',\n",
" description='Test Item description',\n",
" price=10.15\n",
" )\n",
"\n",
" self.assertEqual(0, Payment.objects.count())\n",
"\n",
" payment = item_buy(user=user, item=item)\n",
"\n",
" self.assertEqual(1, Payment.objects.count())\n",
" self.assertEqual(payment, Payment.objects.first())\n",
"\n",
" self.assertFalse(payment.successful)\n",
"\n",
" payment_charge_mock.assert_called_once()"
],
"id": "18e48e3a427946f3"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# APIs & Serializers\n",
"\n",
"وقتی از **Services** و **Selectors** استفاده می‌کنیم، تمام APIهامون باید ظاهری ساده و یکسان داشته باشن.\n",
"\n",
"---\n",
"\n",
"## اصول کلی در ساخت APIها\n",
"\n",
"1. **هر عملیات = یک API**\n",
" برای مثال در CRUD، چهار API جدا برای Create, Read, Update, Delete داریم.\n",
"\n",
"2. **ارث‌بری از ساده‌ترین کلاس ممکن**\n",
" از `APIView` یا `GenericAPIView` استفاده کنید.\n",
" کلاس‌های خیلی انتزاعی‌تر (مثل ViewSets) کارها رو با Serializer هندل می‌کنن، در حالی که ما می‌خوایم منطق اصلی رو در Service و Selector نگه داریم.\n",
"\n",
"3. **عدم وجود Business Logic در API**\n",
" نباید منطق اصلی در API نوشته بشه.\n",
"\n",
"4. **مدیریت داده درون API مجازه**\n",
" کارهایی مثل واکشی آبجکت یا دست‌کاری داده می‌تونه داخل API انجام بشه (البته می‌تونید اون رو به تابع یا Service جداگانه منتقل کنید).\n",
"\n",
"5. **سادگی در طراحی APIها**\n",
" API فقط باید رابطی باشه بین کاربر و منطق اصلی برنامه.\n",
" در صورت نیاز، می‌تونید از توابعی مثل `some_service_parse` برای جداسازی مرحله‌ی آماده‌سازی داده‌ها استفاده کنید.\n",
"\n",
"---\n",
"\n",
"## Serializers\n",
"\n",
"در کنار APIها، ما برای کار با داده‌ها — چه **ورودی** و چه **خروجی** — به Serializer نیاز داریم.\n",
"\n",
"Serializer وظیفه‌ی زیر رو داره:\n",
"- اعتبارسنجی داده‌های ورودی (Validation)\n",
"- تبدیل داده‌ها به ساختارهای Python برای استفاده در Service یا Database\n",
"- آماده‌سازی داده‌های خروجی برای ارسال به Client\n",
"\n",
"---\n",
"\n",
" در حالت کلی:\n",
"- **Service** و **Selector** منطق اصلی (Business Logic) رو انجام می‌دن.\n",
"- **Serializer** فقط وظیفه‌ی تبدیل و اعتبارسنجی داده رو داره.\n",
"- **API** نقش واسطه‌ی بین این دو رو بازی می‌کنه.\n"
],
"id": "74789ff48a6ed3a0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# API Serialization Rules\n",
"\n",
"وقتی صحبت از **سریالایزرها** میشه، چند قانون کلی داریم که کار رو تمیز و قابل پیش‌بینی نگه می‌دارن:\n",
"\n",
"---\n",
"\n",
"## اصول پایه\n",
"\n",
"1. برای هر API باید **دو سریالایزر جداگانه** وجود داشته باشه:\n",
" - `InputSerializer` → داده‌های ورودی رو اعتبارسنجی و آماده می‌کنه.\n",
" - `OutputSerializer` → داده‌های خروجی رو برای نمایش آماده می‌کنه.\n",
"\n",
"2. از هر انتزاعی (abstraction) که دوست داری استفاده کن — هر چیزی که برات کارآمده.\n",
"\n",
"---\n",
"\n",
"## اگر از DRF (Django REST Framework) استفاده می‌کنی\n",
"\n",
"1. **سریالایزرها باید داخل API تعریف بشن**\n",
" و با نام‌های `InputSerializer` و `OutputSerializer` شناخته بشن.\n",
" این باعث میشه ساختار کد واضح‌تر و محلی‌تر باشه.\n",
"\n",
"2. **ترجیحاً از `Serializer` به‌جای `ModelSerializer` استفاده کن**\n",
" `Serializer` کنترل بیشتری بهت میده و API رو از مدل جدا نگه می‌داره.\n",
" اما اگر `ModelSerializer` برای پروژه‌ت جواب میده، هیچ اشکالی نداره ازش استفاده کنی.\n",
"\n",
"3. **در صورت نیاز به nested serializer**\n",
" از ابزار `inline_serializer` استفاده کن تا ساختار کد تمیز و ایزوله بمونه.\n",
"\n",
"4. **از Reuse بیش‌ازحد سریالایزرها خودداری کن**\n",
" چون تغییر در یک سریالایزر پایه ممکنه رفتار APIهای دیگه رو هم ناخواسته تغییر بده.\n",
"\n",
"---\n",
"\n",
"خلاصه:\n",
"> هر API = ۱ سریالایزر ورودی + ۱ سریالایزر خروجی\n",
"> و هر دو داخل همان View تعریف بشن تا ساختار کد شفاف و محلی باقی بمونه.\n"
],
"id": "a717874e312d26ad"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Class-based vs. Function-based APIs\n",
"\n",
"انتخاب بین **Class-based** و **Function-based** معمولاً به سلیقه و عادت تیم بستگی داره،\n",
"چون هر دو روش می‌تونن دقیقا همون نتایج رو بدن.\n",
"\n",
"---\n",
"\n",
"- به‌صورت پیش‌فرض از **Class-based APIs / Views** استفاده می‌کنیم.\n",
"- اگر بقیه اعضای تیم با **Function-based** راحت‌تر هستن، از اون روش استفاده کنید.\n",
"\n",
"---\n",
"\n",
"## مزایای استفاده از Class-based\n",
"\n",
"1. می‌تونی از `BaseApi` ارث‌بری کنی یا از **mixins** استفاده کنی.\n",
"2. اگر Function-based باشی، باید همین رفتار رو با **decorator**ها بسازی.\n",
"3. کلاس خودش یک **namespace** ایجاد می‌کنه، که می‌تونی داخلش\n",
" متدها، اتربیوت‌ها و تنظیمات مختلف رو بنویسی.\n",
"4. در کلاس‌ها می‌تونی پیکربندی API رو از طریق **class attributes** انجام بدی.\n",
" ولی در حالت تابعی، باید **decorator**ها رو روی هم بچینی.\n",
"\n",
"---\n",
"\n",
" **خلاصه:**\n",
"کلاس‌ها ساختار تمیزتری برای گسترش، تست و پیکربندی فراهم می‌کنن،\n",
"اما اگر تیم با توابع احساس راحتی بیشتری داره، مهم‌تر از همه **یکنواختی و هماهنگی در کد**ه.\n"
],
"id": "2c6c3c80abaf825b"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "با کلاس مثال `BaseApi`",
"id": "9f4ad8234dabaf2a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class SomeApi(BaseApi):\n",
" def get(self, request):\n",
" data = something()\n",
"\n",
" return Response(data)"
],
"id": "9f94608de6174efa"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " یک مثال `functionBase`",
"id": "e94df60d317d3e3"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class SomeApi(BaseApi):\n",
" def get(self, request):\n",
" data = something()\n",
"\n",
" return Response(data)"
],
"id": "b49c79da5f621ecb"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " با `BaseApi decorator`",
"id": "4b5346f62f553f0e"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"@base_api([\"GET\"])\n",
"def some_api(request):\n",
" data = something()\n",
" return Response(data)"
],
"id": "671ce724fddcdd05"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# List APIs\n",
"\n",
"بریم سراغ ساده‌ترین نوع API یعنی **List API** — اونایی که فقط یه لیست از دیتا برمی‌گردونن، بدون دردسر خاص.\n",
"\n",
"خب، یه **plain list API** معمولاً خیلی تمیز و جمع‌وجوره. چیزی شبیه به مثال زیره"
],
"id": "159ba4d84086654f"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework.views import APIView\n",
"from rest_framework import serializers\n",
"from rest_framework.response import Response\n",
"\n",
"from styleguide_example.users.selectors import user_list\n",
"from styleguide_example.users.models import BaseUser\n",
"\n",
"\n",
"class UserListApi(APIView):\n",
" class OutputSerializer(serializers.Serializer):\n",
" id = serializers.CharField()\n",
" email = serializers.CharField()\n",
"\n",
" def get(self, request):\n",
" users = user_list()\n",
"\n",
" data = self.OutputSerializer(users, many=True).data\n",
"\n",
" return Response(data)"
],
"id": "beb124af9c72d8b"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " >یادت باشه این پابلیکه Authentication اش با خودته",
"id": "dc5f66e599ce8eca"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Filters + Pagination\n",
"\n",
"اولش شاید به نظر سخت بیاد، چون API ما از همون plain `APIView` تو DRF ارث می‌بره،\n",
"در حالی که filtering و pagination تو کلاس‌های generic خودشون تعبیه شدن.\n",
"\n",
"- **DRF Filtering**\n",
"- **DRF Pagination**\n",
"\n",
"---\n",
"\n",
"## رویکرد ما\n",
"\n",
"1. **Selectors** مسئول فیلتر واقعی هستن.\n",
" یعنی کد اصلی که دیتا رو فیلتر می‌کنه، توی selector میره.\n",
"\n",
"2. **APIs** مسئول **serialize کردن پارامترهای فیلتر** هستن.\n",
" یعنی ورودی رو می‌گیرن و آماده می‌کنن که selector بتونه استفاده کنه.\n",
"\n",
"3. اگر می‌خوای از paginationهای آماده DRF استفاده کنی،\n",
" خود API اون رو هندل می‌کنه.\n",
"\n",
"4. اگر دنبال یه pagination متفاوتی، یا پیاده‌سازی خودت هستی:\n",
" - می‌تونی یه لایه جدید بسازی برای pagination،\n",
" - یا بذاری selector این کار رو انجام بده.\n",
"\n",
"---\n",
"\n",
"نکته اصلی:\n",
"**API فقط رابط باشه، منطق و دیتا رو بذار توی selector و service** —\n",
"نه اینکه همه چیز توی API جمع بشه و کد شلخته شه.\n"
],
"id": "6be4e4c7a3811b29"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework.views import APIView\n",
"from rest_framework import serializers\n",
"\n",
"from styleguide_example.api.mixins import ApiErrorsMixin\n",
"from styleguide_example.api.pagination import get_paginated_response, LimitOffsetPagination\n",
"\n",
"from styleguide_example.users.selectors import user_list\n",
"from styleguide_example.users.models import BaseUser\n",
"\n",
"\n",
"class UserListApi(ApiErrorsMixin, APIView):\n",
" class Pagination(LimitOffsetPagination):\n",
" default_limit = 1\n",
"\n",
" class FilterSerializer(serializers.Serializer):\n",
" id = serializers.IntegerField(required=False)\n",
" # Important: If we use BooleanField, it will default to False\n",
" is_admin = serializers.NullBooleanField(required=False)\n",
" email = serializers.EmailField(required=False)\n",
"\n",
" class OutputSerializer(serializers.Serializer):\n",
" id = serializers.CharField()\n",
" email = serializers.CharField()\n",
" is_admin = serializers.BooleanField()\n",
"\n",
" def get(self, request):\n",
" # Make sure the filters are valid, if passed\n",
" filters_serializer = self.FilterSerializer(data=request.query_params)\n",
" filters_serializer.is_valid(raise_exception=True)\n",
"\n",
" users = user_list(filters=filters_serializer.validated_data)\n",
"\n",
" return get_paginated_response(\n",
" pagination_class=self.Pagination,\n",
" serializer_class=self.OutputSerializer,\n",
" queryset=users,\n",
" request=request,\n",
" view=self\n",
" )"
],
"id": "7c855fe41f81d7b0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# API Walkthrough\n",
"\n",
"وقتی به API نگاه می‌کنیم، چند تا چیز مشخص می‌شه:\n",
"\n",
"1. **FilterSerializer** داریم، که پارامترهای query رو هندل می‌کنه.\n",
" اگه این کارو اینجا نکنیم، باید جای دیگه هندل بشه و خب،\n",
" DRF serializers تو این کار عالی هستن.\n",
"\n",
"2. بعد از serialize کردن، فیلترها رو می‌فرستیم به **user_list selector**.\n",
" یعنی دیتا واقعا از اینجا فیلتر می‌شه.\n",
"\n",
"3. برای خروجی، از **get_paginated_response** استفاده می‌کنیم،\n",
" تا یه پاسخ paginated (صفحه‌بندی شده) به کلاینت بدیم.\n",
"\n",
"---\n",
"\n",
" خلاصه:\n",
"API نقش **واسطه** بین پارامترهای کاربر و منطق اصلی (selector) رو داره،\n",
"نه اینکه خودش دیتا رو دستکاری کنه یا فیلترهای پیچیده بسازه.\n"
],
"id": "307e1ab2f3730fcd"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"import django_filters\n",
"\n",
"from styleguide_example.users.models import BaseUser\n",
"\n",
"\n",
"class BaseUserFilter(django_filters.FilterSet):\n",
" class Meta:\n",
" model = BaseUser\n",
" fields = ('id', 'email', 'is_admin')\n",
"\n",
"\n",
"def user_list(*, filters=None):\n",
" filters = filters or {}\n",
"\n",
" qs = BaseUser.objects.all()\n",
"\n",
" return BaseUserFilter(filters, qs).qs"
],
"id": "997a2cc32be54ba2"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Using django-filter\n",
"\n",
"همون‌طور که می‌بینی، ما داریم از **کتابخونه قدرتمند `django-filter`** استفاده می‌کنیم.\n",
"\n",
"---\n",
"\n",
" نکته اصلی:\n",
"**selector مسئول فیلتر واقعی هستش**.\n",
"می‌تونی همیشه از چیز دیگه‌ای به عنوان abstraction برای فیلتر استفاده کنی،\n",
"اما برای بیشتر موارد، `django-filter` کامل کافی و راحت هست.\n"
],
"id": "2fbd676f6c42fa9"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " get_paginated_response :",
"id": "e5843d5c23bf4c94"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework.response import Response\n",
"\n",
"\n",
"def get_paginated_response(*, pagination_class, serializer_class, queryset, request, view):\n",
" paginator = pagination_class()\n",
"\n",
" page = paginator.paginate_queryset(queryset, request, view=view)\n",
"\n",
" if page is not None:\n",
" serializer = serializer_class(page, many=True)\n",
" return paginator.get_paginated_response(serializer.data)\n",
"\n",
" serializer = serializer_class(queryset, many=True)\n",
"\n",
" return Response(data=serializer.data)"
],
"id": "e735db438d1a09ea"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "LimitOffsetPagination :",
"id": "daae4b408a8d2df"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from collections import OrderedDict\n",
"\n",
"from rest_framework.pagination import LimitOffsetPagination as _LimitOffsetPagination\n",
"from rest_framework.response import Response\n",
"\n",
"\n",
"class LimitOffsetPagination(_LimitOffsetPagination):\n",
" default_limit = 10\n",
" max_limit = 50\n",
"\n",
" def get_paginated_data(self, data):\n",
" return OrderedDict([\n",
" ('limit', self.limit),\n",
" ('offset', self.offset),\n",
" ('count', self.count),\n",
" ('next', self.get_next_link()),\n",
" ('previous', self.get_previous_link()),\n",
" ('results', data)\n",
" ])\n",
"\n",
" def get_paginated_response(self, data):\n",
" \"\"\"\n",
" We redefine this method in order to return `limit` and `offset`.\n",
" This is used by the frontend to construct the pagination itself.\n",
" \"\"\"\n",
" return Response(OrderedDict([\n",
" ('limit', self.limit),\n",
" ('offset', self.offset),\n",
" ('count', self.count),\n",
" ('next', self.get_next_link()),\n",
" ('previous', self.get_previous_link()),\n",
" ('results', data)\n",
" ]))"
],
"id": "2f088f36fd6c1a25"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Reverse-Engineering Generic APIs\n",
"\n",
"در واقع کاری که کردیم اینه که **generic APIs رو reverse-engineer کردیم** و فهمیدیم چطور کار می‌کنن.\n",
"\n",
"---\n",
"\n",
" نکته مهم:\n",
"اگر دنبال یه pagination متفاوت هستی، می‌تونی همیشه خودت پیاده‌سازی کنی و همون روش رو استفاده کنی.\n",
"گاهی اوقات لازم می‌شه selector خودش pagination رو هندل کنه،\n",
"و ما با همین روش filtering اون رو انجام می‌دیم.\n",
"\n",
"---\n",
"\n",
" برای مثال عملی:\n",
"کد یک **List API با filters و pagination** رو می‌تونی توی پروژه **Styleguide Example** پیدا کنی.\n"
],
"id": "f7f562c9f6eed1b2"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "# Detail API",
"id": "7f7dbf4ea0ef2fb5"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class CourseDetailApi(SomeAuthenticationMixin, APIView):\n",
" class OutputSerializer(serializers.Serializer):\n",
" id = serializers.CharField()\n",
" name = serializers.CharField()\n",
" start_date = serializers.DateField()\n",
" end_date = serializers.DateField()\n",
"\n",
" def get(self, request, course_id):\n",
" course = course_get(id=course_id)\n",
"\n",
" serializer = self.OutputSerializer(course)\n",
"\n",
" return Response(serializer.data)"
],
"id": "2ef277dfbab647d"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "# Create API",
"id": "302129ac4d79bb9b"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class CourseCreateApi(SomeAuthenticationMixin, APIView):\n",
" class InputSerializer(serializers.Serializer):\n",
" name = serializers.CharField()\n",
" start_date = serializers.DateField()\n",
" end_date = serializers.DateField()\n",
"\n",
" def post(self, request):\n",
" serializer = self.InputSerializer(data=request.data)\n",
" serializer.is_valid(raise_exception=True)\n",
"\n",
" course_create(**serializer.validated_data)\n",
"\n",
" return Response(status=status.HTTP_201_CREATED)"
],
"id": "a1d089fc6aefa951"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "# Update API",
"id": "c62cb632347346a0"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class CourseUpdateApi(SomeAuthenticationMixin, APIView):\n",
" class InputSerializer(serializers.Serializer):\n",
" name = serializers.CharField(required=False)\n",
" start_date = serializers.DateField(required=False)\n",
" end_date = serializers.DateField(required=False)\n",
"\n",
" def post(self, request, course_id):\n",
" serializer = self.InputSerializer(data=request.data)\n",
" serializer.is_valid(raise_exception=True)\n",
"\n",
" course_update(course_id=course_id, **serializer.validated_data)\n",
"\n",
" return Response(status=status.HTTP_200_OK)"
],
"id": "13675cdd9219e1a9"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Fetching Objects\n",
"\n",
"وقتی API ما یه `object_id` می‌گیره، سوال اینه: **کجا باید اون object رو fetch کنیم؟**\n",
"\n",
"---\n",
"\n",
"## گزینه‌ها\n",
"\n",
"1. می‌تونیم اون object رو به **serializer** بدیم، که خودش یه `PrimaryKeyRelatedField` یا حتی `SlugRelatedField` داره.\n",
"2. می‌تونیم یه جور object fetching توی **API** انجام بدیم و بعد object رو به **service یا selector** بدیم.\n",
"3. می‌تونیم همون `id` رو به **service/selector** بدیم و fetch اونجا انجام بشه.\n",
"\n",
"---\n",
"\n",
" تصمیم اینکه کدوم روش رو استفاده کنیم، **بستگی به context پروژه و سلیقه تیم** داره.\n",
"\n",
"---\n",
"\n",
"## روشی که ما معمولا استفاده می‌کنیم\n",
"\n",
"معمولا objectها رو تو سطح **API** fetch می‌کنیم، با استفاده از یه util مخصوص به اسم `get_object`.\n"
],
"id": "81c67183d7f2c7f5"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def get_object(model_or_queryset, **kwargs):\n",
" \"\"\"\n",
" Reuse get_object_or_404 since the implementation supports both Model && queryset.\n",
" Catch Http404 & return None\n",
" \"\"\"\n",
" try:\n",
" return get_object_or_404(model_or_queryset, **kwargs)\n",
" except Http404:\n",
" return None"
],
"id": "6a1bfe827e5afc3"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " این یک یوتیلیتی ساده است که استثنا رو هندل میکنه فقط حواست باشه که جوابا یک دست باشه",
"id": "57f45ca644fbac86"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "# Nested serializers",
"id": "89cceae953a1532c"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class Serializer(serializers.Serializer):\n",
" weeks = inline_serializer(many=True, fields={\n",
" 'id': serializers.IntegerField(),\n",
" 'number': serializers.IntegerField(),\n",
" })"
],
"id": "a8fe5e4ad714459a"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Advanced Serialization\n",
"\n",
"گاهی اوقات، نتیجه نهایی یک API می‌تونه خیلی پیچیده باشه.\n",
"گاهی وقتا می‌خواهیم **queryها رو بهینه کنیم** و خود این بهینه‌سازی‌ها می‌تونه خیلی پیچیده بشه.\n",
"\n",
"\n",
"\n",
"**اگر فقط بخواهیم از یه OutputSerializer استفاده کنیم، ممکنه انتخاب‌ها و گزینه‌هایمون محدود بشه.**\n",
"\n",
"\n",
"\n",
"در این مواقع، می‌تونیم **serialization خروجی رو به صورت یک تابع پیاده‌سازی کنیم** و تمام بهینه‌سازی‌هایی که نیاز داریم رو اونجا انجام بدیم، به جای اینکه همه‌ی بهینه‌سازی‌ها رو توی selector انجام بدیم.\n",
"\n",
"\n"
],
"id": "5b00ef8732d49c97"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class SomeGenericFeedApi(BaseApi):\n",
" def get(self, request):\n",
" feed = some_feed_get(\n",
" user=request.user,\n",
" )\n",
"\n",
" data = some_feed_serialize(feed)\n",
"\n",
" return Response(data)"
],
"id": "a19c6a1f49270cee"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"## Example: Advanced Serialization Function\n",
"\n",
"در این سناریو، `some_feed_get` مسئول بازگرداندن یک لیست از آیتم‌های feed هستش (ممکنه ORM objectها باشن، ممکنه فقط IDها باشن، یا هر چی که برای شما مناسب باشه).\n",
"\n",
"---\n",
"\n",
"ما می‌خواهیم پیچیدگی **serialization** این feed رو، به روشی بهینه، به تابع serializer بسپاریم - یعنی `some_feed_serialize`.\n",
"\n",
"---\n",
"\n",
"این به این معنیه که دیگه نیازی نیست که **پیش‌فرض‌ها (prefetch)** و بهینه‌سازی‌های غیرضروری رو توی `some_feed_get` انجام بدیم.\n",
"\n",
"---\n"
],
"id": "40d0e10f57d465bc"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class FeedItemSerializer(serializers.Serializer):\n",
" ... some fields here ...\n",
" calculated_field = serializers.IntegerField(source=\"_calculated_field\")\n",
"\n",
"\n",
"def some_feed_serialize(feed: List[FeedItem]):\n",
" feed_ids = [feed_item.id for feed_item in feed]\n",
"\n",
" # Refetch items with more optimizations\n",
" # Based on the relations that are going in\n",
" objects = FeedItem.objects.select_related(\n",
" # ... as complex as you want ...\n",
" ).prefetch_related(\n",
" # ... as complex as you want ...\n",
" ).filter(\n",
" id__in=feed_ids\n",
" ).order_by(\n",
" \"-some_timestamp\"\n",
" )\n",
"\n",
" some_cache = get_some_cache(feed_ids)\n",
"\n",
" result = []\n",
"\n",
" for feed_item in objects:\n",
" # An example, adding additional fields for the serializer\n",
" # That are based on values outside of our current object\n",
" # This may be some optimization to save queries\n",
" feed_item._calculated_field = some_cache.get(feed_item.id)\n",
"\n",
" result.append(FeedItemSerializer(feed_item).data)\n",
"\n",
" return result"
],
"id": "40921757eeb3e20d"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"همونطور که می‌بینید، این یه مثال خیلی **همگانی** هستش، ولی ایده خیلی ساده‌ست:\n",
"\n",
"---\n",
"\n",
"- داده‌هاتون رو دوباره با **joins** و **prefetch** لازم می‌گیرید.\n",
"- کش‌های درون‌حافظه‌ای (in-memory caches) رو می‌سازید که برای مقادیر محاسبه شده، تعداد queryها رو کاهش می‌ده.\n",
"- در نهایت یه **نتیجه‌ای که آماده‌ی پاسخ‌دهی API باشه** برمی‌گردونید.\n",
"\n",
"---\n",
"\n",
"حتی با اینکه به این روش اسم \"**advanced serialization**\" داده شده، این الگو خیلی قدرتمنده و می‌تونید برای همه‌ی انواع serialization ها ازش استفاده کنید.\n",
"\n",
"---\n",
"\n",
"این تابع‌های serializer معمولاً در **ماژول serializers.py** در اپ مربوطه‌ی **Django** قرار می‌گیرن.\n"
],
"id": "23edc698dc71ffbc"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def user_list(*, fetched_by: User) -> Iterable[User]:\n",
" user_ids = user_get_visible_for(user=fetched_by)\n",
"\n",
" query = Q(id__in=user_ids)\n",
"\n",
" return User.objects.filter(query)"
],
"id": "2f05efa0947ede97"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# URLs\n",
"\n",
"ما معمولاً **URLها رو دقیقاً مثل APIها** سازمان‌دهی می‌کنیم — یعنی **هر API یک URL مخصوص خودش داره**.\n",
"به زبون ساده‌تر:\n",
"> هر \"action\" توی سیستم، یه مسیر (route) جداگانه داره.\n",
"\n",
"---\n",
"\n",
"## نکته‌ی مهم\n",
"بهتره URLهای مربوط به هر دامین (domain یا بخش از پروژه) رو داخل یه لیست جدا مثل `domain_patterns` بنویسیم\n",
"و بعد با `include()` از `urlpatterns` اصلی فراخوانی کنیم.\n",
"\n",
"---\n"
],
"id": "95a62e4d7f565154"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.urls import path, include\n",
"\n",
"from project.education.apis import (\n",
" CourseCreateApi,\n",
" CourseUpdateApi,\n",
" CourseListApi,\n",
" CourseDetailApi,\n",
" CourseSpecificActionApi,\n",
")\n",
"\n",
"\n",
"course_patterns = [\n",
" path('', CourseListApi.as_view(), name='list'),\n",
" path('<int:course_id>/', CourseDetailApi.as_view(), name='detail'),\n",
" path('create/', CourseCreateApi.as_view(), name='create'),\n",
" path('<int:course_id>/update/', CourseUpdateApi.as_view(), name='update'),\n",
" path(\n",
" '<int:course_id>/specific-action/',\n",
" CourseSpecificActionApi.as_view(),\n",
" name='specific-action'\n",
" ),\n",
"]\n",
"\n",
"urlpatterns = [\n",
" path('courses/', include((course_patterns, 'courses'))),\n",
"]"
],
"id": "45b1d0ffeadb4a80"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Splitting URLs\n",
"\n",
"تقسیم کردن URLها به این شکل، بهت **انعطاف بیشتری** می‌ده.\n",
"به‌خصوص توی پروژه‌های بزرگ که فایل `urls.py` شلوغ می‌شه و حتی ممکنه دائم **conflict موقع merge** پیش بیاد.\n",
"با جدا کردن هر domain به یه ماژول مستقل، می‌تونی راحت‌تر کنترلش کنی یا حتی بعداً جابه‌جاش کنی.\n",
"\n",
"---\n",
"\n",
"## دیدن کل درخت URL\n",
"\n",
"اگه دوست داری **ساختار کامل درخت URL** پروژه رو یک‌جا ببینی،\n",
"کافیه متغیرهای جداگانه برای `include()` تعریف نکنی و مستقیماً اون‌ها رو بنویسی.\n",
"\n",
"---\n"
],
"id": "8b910c7f578207ea"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.urls import path, include\n",
"\n",
"from styleguide_example.files.apis import (\n",
" FileDirectUploadApi,\n",
"\n",
" FilePassThruUploadStartApi,\n",
" FilePassThruUploadFinishApi,\n",
" FilePassThruUploadLocalApi,\n",
")\n",
"\n",
"\n",
"urlpatterns = [\n",
" path(\n",
" \"upload/\",\n",
" include(([\n",
" path(\n",
" \"direct/\",\n",
" FileDirectUploadApi.as_view(),\n",
" name=\"direct\"\n",
" ),\n",
" path(\n",
" \"pass-thru/\",\n",
" include(([\n",
" path(\n",
" \"start/\",\n",
" FilePassThruUploadStartApi.as_view(),\n",
" name=\"start\"\n",
" ),\n",
" path(\n",
" \"finish/\",\n",
" FilePassThruUploadFinishApi.as_view(),\n",
" name=\"finish\"\n",
" ),\n",
" path(\n",
" \"local/<str:file_id>/\",\n",
" FilePassThruUploadLocalApi.as_view(),\n",
" name=\"local\"\n",
" )\n",
" ], \"pass-thru\"))\n",
" )\n",
" ], \"upload\"))\n",
" )\n",
"]"
],
"id": "73c1f15760ae7ea8"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Settings Structure\n",
"\n",
"وقتی صحبت از تنظیمات (Settings) جنگو می‌شه، ما معمولاً از ساختار پوشه‌ای **cookiecutter-django** پیروی می‌کنیم،\n",
"با چند تغییر کوچک که کار رو تمیزتر می‌کنه\n",
"\n",
"---\n",
"\n",
"## اصول کلی\n",
"\n",
"- تنظیمات خاص جنگو رو از بقیه تنظیمات جدا می‌کنیم.\n",
"- همه‌چیز باید در `base.py` تعریف و **در اون فایل import** بشه.\n",
"- فایل `production.py` نباید تنظیمات خاص خودش رو داشته باشه.\n",
"- هرچیزی که فقط باید تو محیط production فعال باشه،\n",
" باید **با environment variable** کنترل بشه، نه با شرط مستقیم داخل کد.\n",
"\n",
"---\n",
"\n",
"## ساختار پیشنهادی پوشه‌ها\n",
"\n",
"یه نمونه از ساختار پروژه‌ی ما (برگرفته از *Styleguide Example*):\n",
"\n",
"```bash\n",
"config\n",
"├── __init__.py\n",
"├── django\n",
"│   ├── __init__.py\n",
"│   ├── base.py\n",
"│   ├── local.py\n",
"│   ├── production.py\n",
"│   └── test.py\n",
"├── settings\n",
"│   ├── __init__.py\n",
"│   ├── celery.py\n",
"│   ├── cors.py\n",
"│   ├── sentry.py\n",
"│   └── sessions.py\n",
"├── urls.py\n",
"├── env.py\n",
"└── wsgi.py\n",
"├── asgi.py"
],
"id": "215f2485c32a69ca"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Django Settings Layout\n",
"\n",
"وقتی ساختار تنظیمات رو طراحی می‌کنیم، هدف اینه که **چیزهای مربوط به جنگو** از **بقیه تنظیمات پروژه** جدا باشن.\n",
"ما معمولاً این تفکیک رو به دو بخش انجام می‌دیم:\n",
"`config/django` برای تنظیمات خود جنگو و `config/settings` برای تنظیمات جانبی.\n",
"\n",
"---\n",
"\n",
"## داخل `config/django`\n",
"\n",
"اینجا همه‌چیز مستقیماً به جنگو مربوطه:\n",
"\n",
"- **`base.py`**\n",
" شامل بیشتر تنظیماته و بقیه فایل‌ها ازش import می‌گیرن.\n",
" (قلب تنظیمات اصلی پروژه‌ست )\n",
"\n",
"- **`production.py`**\n",
" از `base.py` import می‌کنه و فقط مقادیر خاص محیط production رو **بازنویسی (override)** می‌کنه.\n",
"\n",
"- **`test.py`**\n",
" از `base.py` import می‌کنه و تنظیمات مخصوص تست‌ها رو تغییر می‌ده.\n",
" این فایل باید به عنوان ماژول تنظیمات توی `pytest.ini` ست بشه.\n",
"\n",
"- **`local.py`**\n",
" از `base.py` import می‌کنه و برای توسعه‌ی لوکال استفاده می‌شه.\n",
" اگه خواستی ازش استفاده کنی، باید توی `manage.py` بهش اشاره کنی،\n",
" وگرنه همون `base.py` کفایت می‌کنه.\n",
"\n",
"---\n",
"\n",
"## داخل `config/settings`\n",
"\n",
"اینجا تنظیمات **غیرمستقیم جنگویی** قرار می‌گیرن — مثل:\n",
"\n",
"- تنظیمات **Celery**\n",
"- تنظیمات **third-party packages**\n",
"- هر نوع تنظیم عمومی دیگه‌ای که به خود جنگو وابسته نیست\n",
"\n",
"---\n",
"\n",
" **نتیجه:**\n",
"با این ساختار، یه تفکیک تمیز بین هسته‌ی جنگو و تنظیمات خارجی داری.\n",
"یعنی هر بخش دقیقاً می‌دونه جاش کجاست و چی رو باید کنترل کنه\n"
],
"id": "d5cd6a887170fd0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " به علاوه ما از `cogig/env.py`",
"id": "ea4618b8cf736e41"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"import environ\n",
"\n",
"env = environ.Env()"
],
"id": "eaa69de4400fe7cd"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " و اگر ما از محیط بخواهیم چیزی دریافت کنیم",
"id": "831eed74d468d413"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "from config.env import env",
"id": "8c49910f9c5639ad"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"معمولاً در انتهای فایل **`base.py`**،\n",
"تمام تنظیمات دیگه رو از مسیر **`config/settings`** ایمپورت می‌کنیم.\n",
"اینجوری همه‌ی کانفیگ‌های جانبی (مثل Celery یا تنظیمات تردپارتی‌ها)\n",
"به‌صورت خودکار لود می‌شن و توی محیط جنگو قابل استفاده‌ان."
],
"id": "31306ba2b26f769c"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from config.settings.cors import * # noqa\n",
"from config.settings.sessions import * # noqa\n",
"from config.settings.celery import * # noqa\n",
"from config.settings.sentry import * # noqa"
],
"id": "f743350624d75c39"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Prefix کردن Environment Variables با DJANGO_\n",
"\n",
"شاید توی خیلی مثال‌ها دیده باشی که Environment Variables رو با **DJANGO_** شروع می‌کنن.\n",
"این کار مخصوصاً وقتی مفیده که چند تا برنامه‌ی دیگه هم روی همون محیط اجرا میشن.\n",
"با این prefix، راحت می‌تونی تشخیص بدی کدوم متغیر برای جنگوئه و کدوم برای بقیه برنامه‌ها.\n",
"\n",
"---\n",
"\n",
"تو **HackSoft** معمولاً چند تا اپ روی یه محیط نداریم،\n",
"پس ما معمولاً فقط متغیرهای خاص جنگو رو با **DJANGO_** شروع می‌کنیم،\n",
"و بقیه چیزها مثل AWS یا Celery بدون prefix میمونن.\n",
"\n",
"مثال:\n",
"\n",
"- Prefix می‌کنیم:\n",
" `DJANGO_SETTINGS_MODULE`, `DJANGO_DEBUG`, `DJANGO_ALLOWED_HOSTS`, `DJANGO_CORS_ORIGIN_WHITELIST`\n",
"- Prefix نمی‌کنیم:\n",
" `AWS_SECRET_KEY`, `CELERY_BROKER_URL`, `EMAILS_ENABLED`\n",
"\n",
"---\n",
"\n",
"⚠️ این کاملاً سلیقه‌ایه، ولی **یکسان بودنش تو پروژه مهمه**.\n",
"یک قانون مشخص داشته باش و همه جا رعایتش کن.\n"
],
"id": "16b9c1a5440f5f00"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Integrations\n",
"\n",
"خب، از اونجایی که همه چیز باید توی **base.py** ایمپورت بشه،\n",
"ولی بعضی وقت‌ها نمی‌خوایم یه integration خاص رو برای توسعه لوکال کانفیگ کنیم،\n",
"ما این روش رو گرفتیم:\n",
"\n",
"1. تنظیمات مربوط به هر integration رو می‌ذاریم توی\n",
" `config/settings/some_integration.py`\n",
"2. همیشه یه boolean داریم به اسم **`USE_SOME_INTEGRATION`**\n",
" که از environment می‌خونه و پیش‌فرضش **False** هست.\n",
"3. اگه مقدارش **True** بود، بقیه‌ی تنظیمات رو هم می‌خونه و اگه چیزی کم بود، خطا میده.\n",
"\n",
"---\n",
"\n",
" خلاصه‌ش اینکه:\n",
"- لوکال راحت باشه، همه چیز default باشه.\n",
"- وقتی بخوای production یا staging یه integration فعال کنی، همه چیز آماده باشه و خطاها واضح باشن.\n"
],
"id": "ea341bb03febfa6"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from config.env import env\n",
"\n",
"SENTRY_DSN = env('SENTRY_DSN', default='')\n",
"\n",
"if SENTRY_DSN:\n",
" import sentry_sdk\n",
" from sentry_sdk.integrations.django import DjangoIntegration\n",
" from sentry_sdk.integrations.celery import CeleryIntegration\n",
"\n",
" # ... we proceed with sentry settings here ...\n",
" # View the full file here - https://github.com/HackSoftware/Styleguide-Example/blob/master/config/settings/sentry.py"
],
"id": "5475466b1bb09713"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# .env & Environment Variables\n",
"\n",
"حالا می‌تونی یه فایل **`.env`** توی روت پروژه داشته باشی\n",
"و مقادیر تنظیماتت رو اونجا بذاری (ولی اجباری نیست).\n",
"\n",
"چند نکته مهم:\n",
"\n",
"1. **هیچوقت `.env` رو توی source control نذار**\n",
" چون اطلاعات حساس و credentials میره بیرون\n",
"2. بهتره یه **`.env.example`** داشته باشی\n",
" با مقادیر خالی برای همه چیز، تا توسعه‌دهنده‌های جدید بفهمن چی استفاده می‌شه.\n",
"\n",
"---\n",
"\n",
"# Errors & Exception Handling\n",
"\n",
"اوه پسر، این موضوع بزرگه\n",
"جزئیاتش معمولاً مخصوص هر پروژه هست.\n",
"\n",
"به همین دلیل ما دو بخش می‌کنیم:\n",
"\n",
"1. **Guidelines عمومی**\n",
" - بدون که exception handling چطور کار می‌کنه (با DRF مثال می‌زنیم)\n",
" - مشخص کن که خطاهای API قراره چطوری به کاربر نشون داده بشن\n",
" - بدون چطور می‌تونی رفتار پیش‌فرض exception handling رو تغییر بدی\n",
"\n",
"2. **روش‌های خاص**\n",
" - از default exceptions خود DRF استفاده کن، با کمترین تغییر\n",
" - روش پیشنهادی HackSoft\n",
" - اگر دنبال یه استاندارد جهانی هستی، RFC7807 رو نگاه کن:\n",
" [https://datatracker.ietf.org/doc/html/rfc7807](https://datatracker.ietf.org/doc/html/rfc7807)\n",
"\n",
"---\n",
"\n",
"👀 برای کد عملی، می‌تونی به این لینک نگاه کنی:\n",
"[Styleguide-Example exception handlers](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/exception_handlers.py)\n"
],
"id": "86f17356585f990a"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# How Exception Handling Works (DRF Style)\n",
"\n",
"خب پسر، DRF یه **راهنمای خیلی خوب** داره برای اینکه بفهمیم exceptionها چطور هندل می‌شن:\n",
"[DRF Exceptions Guide](https://www.django-rest-framework.org/api-guide/exceptions/)\n",
"\n",
"---\n",
"\n",
"علاوه بر راهنما، یه **diagram مرتب** داریم که روند کار رو نشون می‌ده:\n",
"(می‌تونی اینو توی notebook با تصویر یا SVG بذاری)\n",
"\n",
"---\n",
"\n",
" نکته: ایده اینه که اول بفهمیم جریان exceptionها چطوریه،\n",
"بعد تازه تصمیم بگیریم چه تغییراتی لازمه برای پروژه خودمون.\n"
],
"id": "cc6b0fa3336db0dc"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "![image.png](attachment:0538973d-4fb2-44e8-968e-444832f70ac6.png)",
"id": "61aab097368adbbf",
"attachments": {
"0538973d-4fb2-44e8-968e-444832f70ac6.png": {
"image/png": "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"
}
}
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"خب، اگر **exception handler نتونه exception مورد نظر رو هندل کنه** و `None` برگردونه،\n",
"این باعث می‌شه که یه **Unhandled Exception** رخ بده و نتیجه‌ش **500 Server Error** باشه 😬\n",
"\n",
"این معمولاً خوبه، چون حداقل **خطاها رو سایلنس نمی‌کنه** و مجبور می‌شی بهشون توجه کنی.\n",
"\n",
"---\n",
"\n",
"حالا، چند **quirk و نکته ریز** هست که باید مراقبش باشیم:"
],
"id": "fa8d6844a3db7151"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# DRF's ValidationError\n",
"\n",
"مثلاً، اگر ساده یه `rest_framework.exceptions.ValidationError` پر کنیم مثل این:\n",
"\n",
"```python\n",
"from rest_framework.exceptions import ValidationError\n",
"\n",
"raise ValidationError(\"این یک خطای ولیدیشن ساده است\")\n"
],
"id": "74d9605d847ad80d"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework import exceptions\n",
"\n",
"\n",
"def some_service():\n",
" raise exceptions.ValidationError(\"Some message\")"
],
"id": "e38176cab9be0570"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " محتوای پاسخ قراره یه چی شبیه به این بشه",
"id": "ee1de05a74d35bf"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "[\"Some message\"]",
"id": "26c1681f0a63cfe2"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " و اگه اینجوری بنویسیمش",
"id": "929e80c337cd090d"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework import exceptions\n",
"\n",
"\n",
"def some_service():\n",
" raise exceptions.ValidationError({\"error\": \"Some message\"})"
],
"id": "94adc2896853431"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جوابش قراره این شکلی باشه",
"id": "736cefa52474d89"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"error\": \"Some message\"\n",
"}"
],
"id": "35a1bb24fe817d9c"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
" این همون `detail` هست که تحویل `validation error` دادیم ولی با نوع داده متفاوت\n",
"\n",
"و حالا یخوایم از استثناعات دیکه استقاده کنیم مثل نمونه های داخل خود DRF"
],
"id": "d0102e7e20c2bdcc"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework import exceptions\n",
"\n",
"\n",
"def some_service():\n",
" raise exceptions.NotFound()"
],
"id": "f2e3745c32e37d9e"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " نمونه جواب قراره شبیه به این بشه",
"id": "f5236e5c8fb631af"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": \"Not found.\"\n",
"}"
],
"id": "9def3c6d953bf4d7"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"این کاملا با چیزی که از رفتار ValidationError دیدیم فرق داره و می‌تونه مشکلاتی ایجاد کنه.\n",
"\n",
"تا الان، رفتار پیش‌فرض DRF می‌تونه برامون تولید کنه:\n",
"\n",
"- یه آرایه.\n",
"- یه دیکشنری.\n",
"- یه نتیجه خاص به شکل {\"detail\": \"something\"}.\n",
"\n",
"پس اگه بخوایم از رفتار پیش‌فرض DRF استفاده کنیم، باید حواسمون به این ناسازگاری باشه.\n"
],
"id": "b017d18fbb868baa"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### ValidationError خود Django\n",
"\n",
"حالا، رفتار پیش‌فرض DRF با ValidationError خود Django زیاد خوش‌اخلاق نیست.\n",
"\n",
"این قطعه کد:\n"
],
"id": "94964a1331d33618"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import ValidationError as DjangoValidationError\n",
"\n",
"\n",
"def some_service():\n",
" raise DjangoValidationError(\"Some error message\")"
],
"id": "ae173904c412798e"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"این باعث می‌شه یه استثنا بدون هندل شدن رخ بده و یه خطای ۵۰۰ سرور بدیم.\n",
"\n",
"همین اتفاق وقتی می‌افته که این ValidationError از اعتبارسنجی مدل اومده باشه، مثلا:\n"
],
"id": "7cf5e261f31bc6e8"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" user = BaseUser()\n",
" user.full_clean() # Throws ValidationError\n",
" user.save()"
],
"id": "8af6ba3e918c32ff"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"این هم باعث می‌شه ۵۰۰ سرور بده.\n",
"\n",
"اگه بخوایم شروع کنیم به هندل کردنش، مثل rest_framework.exceptions.ValidationError، باید یه exception handler سفارشی خودمون بسازیم:\n"
],
"id": "89d3e0ef3c12da21"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import ValidationError as DjangoValidationError\n",
"\n",
"from rest_framework.views import exception_handler\n",
"from rest_framework.serializers import as_serializer_error\n",
"from rest_framework import exceptions\n",
"\n",
"\n",
"def custom_exception_handler(exc, ctx):\n",
" if isinstance(exc, DjangoValidationError):\n",
" exc = exceptions.ValidationError(as_serializer_error(exc))\n",
"\n",
" response = exception_handler(exc, ctx)\n",
"\n",
" # If unexpected error occurs (server error, etc.)\n",
" if response is None:\n",
" return response\n",
"\n",
" return response"
],
"id": "62985ff85e2cf595"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "این عملاً پیاده‌سازی پیش‌فرضه، با اضافه شدن این قطعه کد:\n",
"id": "fa6a43b1aca6593a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"if isinstance(exc, DjangoValidationError):\n",
" exc = exceptions.ValidationError(as_serializer_error(exc))"
],
"id": "5711a2bf024527d3"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"چون ما باید بین `django.core.exceptions.ValidationError` و `rest_framework.exceptions.ValidationError` نگاشت انجام بدیم، داریم از `as_serializer_error` خود DRF استفاده می‌کنیم، که توی serializerها هم داخلی استفاده می‌شه، فقط برای همین کار.\n",
"\n",
"با این کار، حالا می‌تونیم ValidationError خود Django رو خوش‌رفتار کنیم و با exception handler خود DRF کار کنه.\n"
],
"id": "b24224f93d41ed23"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# توصیف کنید که خطاهای API شما قراره چه شکلی باشن.\n",
"این خیلی مهمه و بهتره هر چه زودتر توی پروژه مشخص بشه.\n",
"\n",
"به زبان ساده یعنی: با هم توافق کنیم که اینترفیس خطاهای APIمون چه شکلیه وقتی خطایی پیش اومد، خروجی API چه شکلی باشه؟\n",
"\n",
"این خیلی وابسته به پروژه است، می‌تونید از APIهای معروف برای الهام گرفتن استفاده کنید:\n",
"\n",
"- Stripe - [Docs](https://stripe.com/docs/api/errors)\n",
"\n",
"مثلا می‌تونیم تصمیم بگیریم که خطاهای ما اینطوری باشن:\n",
"\n",
"- کد وضعیت 4** و 5** برای انواع مختلف خطاها.\n",
"- هر خطا یه دیکشنری باشه با یه کلید `message` که پیام خطا داخلش باشه.\n"
],
"id": "12ba78fec9982631"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Some error message here\"\n",
"}"
],
"id": "70e26bf54bdb10bc"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"خیلی ساده‌ست:\n",
"\n",
"- 400 برای خطاهای اعتبارسنجی (validation errors)\n",
"- 401 برای خطاهای احراز هویت (auth errors)\n",
"- 403 برای خطاهای دسترسی (permission errors)\n",
"- 404 برای خطاهای پیدا نشدن (not found errors)\n",
"- 429 برای خطاهای محدودسازی درخواست‌ها (throttling errors)\n",
"- 500 برای خطاهای سرور (server errors) اینو باید مواظب باشیم که هیچ استثنایی که باعث 500 میشه رو ساکت نکنیم و همیشه گزارش بدیم، مثلا تو سرویس‌هایی مثل Sentry\n",
"\n",
"باز هم میگم، این بستگی به پروژه داره و شما می‌تونید تغییرش بدید. برای یکی از رویکردهای خاص، یه پیشنهاد مشابه هم ارائه می‌کنیم.\n"
],
"id": "26e4435c50b87b73"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# تغییر رفتار پیش‌فرض مدیریت خطاها\n",
"\n",
"خب ، این یه بخش مهمه: وقتی که تصمیم می‌گیرید خطاهای API شما چه شکلی باشن، باید بتونید رفتار پیش‌فرض مدیریت خطاها رو تغییر بدید.\n",
"\n",
"تو مثال قبل که درباره **ValidationError در Django** صحبت کردیم، یه نمونه‌ی custom exception handler گذاشتیم که دقیقاً همین کارو می‌کنه.\n",
"\n",
"همچنین، تو بخش‌های بعدی چند مثال دیگه هم داریم که می‌تونید ببینید چطوری می‌شه خطاها رو مدیریت کرد.\n"
],
"id": "4783d4ae3b78dc4b"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Approach 1: استفاده از استثناهای پیش‌فرض DRF با تغییرات کم\n",
"\n",
" DRF خودش مدیریت خطاهاش خوبه. ولی یه مشکلی هست: خروجی همیشه consistent نیست.\n",
"پس ما یه سری تغییرات کوچیک می‌زنیم تا همه چیز یکدست بشه.\n",
"\n",
"هدفمون اینه که خطاها همیشه یه شکلی شبیه به این باشن:\n"
],
"id": "81e4e5b0007f52cd"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": \"Some error\"\n",
"}"
],
"id": "ba94876d894cb827"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " یا",
"id": "2249263e32b295f4"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": [\"Some error\", \"Another error\"]\n",
"}"
],
"id": "89d17de795e5abb8"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " یا",
"id": "bc9ea365a5ad64c3"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": { \"key\": \"... some arbitrary nested structure ...\" }\n",
"}"
],
"id": "edbdc83c654e913f"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
" به هرحال مطمعن بشو که ما یک دیکچنری با کلید `detail` داریم\n",
"به علاوه اون ما میخوایم دجنگو `validation error` رو هم حل بکنیم\n",
"برا حل اون مثال کاستوم ما باید این شکلی باشه"
],
"id": "fa0278135c5c78f"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied\n",
"from django.http import Http404\n",
"\n",
"from rest_framework.views import exception_handler\n",
"from rest_framework import exceptions\n",
"from rest_framework.serializers import as_serializer_error\n",
"\n",
"\n",
"def drf_default_with_modifications_exception_handler(exc, ctx):\n",
" if isinstance(exc, DjangoValidationError):\n",
" exc = exceptions.ValidationError(as_serializer_error(exc))\n",
"\n",
" if isinstance(exc, Http404):\n",
" exc = exceptions.NotFound()\n",
"\n",
" if isinstance(exc, PermissionDenied):\n",
" exc = exceptions.PermissionDenied()\n",
"\n",
" response = exception_handler(exc, ctx)\n",
"\n",
" # If unexpected error occurs (server error, etc.)\n",
" if response is None:\n",
" return response\n",
"\n",
" if isinstance(exc.detail, (list, dict)):\n",
" response.data = {\n",
" \"detail\": response.data\n",
" }\n",
"\n",
" return response"
],
"id": "5c2d3bb7cde30bd1"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"خب، ما عملاً یه چیزی شبیه همون exception handler اصلی DRF رو بازسازی می‌کنیم،\n",
"تا بعدش بتونیم با APIException درست کار کنیم (مثلاً دنبال `detail` باشیم).\n",
"\n",
"حالا بزنیم یه سری تست‌ها رو اجرا کنیم:\n"
],
"id": "acb0559469a24997"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" raise DjangoValidationError(\"Some error message\")"
],
"id": "a919a07d7ae367ab"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب",
"id": "9cfc71df3ec7715b"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": {\n",
" \"non_field_errors\": [\"Some error message\"]\n",
" }\n",
"}"
],
"id": "ec89bc4639bac76a"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "f227bc5464940514"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import PermissionDenied\n",
"\n",
"def some_service():\n",
" raise PermissionDenied()"
],
"id": "1c7c107c73c29f86"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب:",
"id": "274a6489eb0727fa"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": \"You do not have permission to perform this action.\"\n",
"}"
],
"id": "3a94f4871bb34"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "47b8822c1b55164f"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.http import Http404\n",
"\n",
"def some_service():\n",
" raise Http404()"
],
"id": "c4062fca5646c5d0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "38d0de7c93edaf10"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": \"Not found.\"\n",
"}"
],
"id": "757df861f03caa79"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "fa809e12cd4eed71"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" raise RestValidationError(\"Some error message\")"
],
"id": "2e538c011629ab88"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "bd409ed104b9c15a"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": [\"Some error message\"]\n",
"}"
],
"id": "83bd5a0aee82662c"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "80b01be1ae215e5c"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" raise RestValidationError(detail={\"error\": \"Some error message\"})"
],
"id": "6b835d50db6bc099"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "38d71423cc829577"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": {\n",
" \"error\": \"Some error message\"\n",
" }\n",
"}"
],
"id": "2a1056196d4cd527"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "95c063f93c357a9f"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class NestedSerializer(serializers.Serializer):\n",
" bar = serializers.CharField()\n",
"\n",
"\n",
"class PlainSerializer(serializers.Serializer):\n",
" foo = serializers.CharField()\n",
" email = serializers.EmailField(min_length=200)\n",
"\n",
" nested = NestedSerializer()\n",
"\n",
"\n",
"def some_service():\n",
" serializer = PlainSerializer(data={\n",
" \"email\": \"foo\",\n",
" \"nested\": {}\n",
" })\n",
" serializer.is_valid(raise_exception=True)\n"
],
"id": "14e08afb10a1b39c"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "96caaabe2eac48b1"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": {\n",
" \"foo\": [\"This field is required.\"],\n",
" \"email\": [\n",
" \"Ensure this field has at least 200 characters.\",\n",
" \"Enter a valid email address.\"\n",
" ],\n",
" \"nested\": {\n",
" \"bar\": [\"This field is required.\"]\n",
" }\n",
" }\n",
"}"
],
"id": "12c5732281d27322"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "9ecf43a33dba5f7b"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework import exceptions\n",
"\n",
"\n",
"def some_service():\n",
" raise exceptions.Throttled()"
],
"id": "db5507641f949da5"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "df93ed692f322a95"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": \"Request was throttled.\"\n",
"}"
],
"id": "73653427ce868ee0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " کد :",
"id": "a3aa36d0e92e8ae0"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" user = BaseUser()\n",
" user.full_clean()"
],
"id": "581c582fe3a391f0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "f78ce4fcdb582fdb"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"detail\": {\n",
" \"password\": [\"This field cannot be blank.\"],\n",
" \"email\": [\"This field cannot be blank.\"]\n",
" }\n",
"}"
],
"id": "5b5fa54a20959264"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"## Approach 2 - روش پیشنهادی HackSoft\n",
"\n",
"خب، میخوایم یه روش معرفی کنیم که راحت قابل توسعه باشه و با پروژه شما جور در بیاد.\n",
"\n",
"ایده‌های کلیدی:\n",
"\n",
"- اپلیکیشن شما یه سلسله‌مراتب از استثناها (exceptions) داره که توسط منطق کسب‌وکار پرتاب میشن.\n",
"- برای ساده بودن، فرض کنیم فقط یه خطا داریم: `ApplicationError`.\n",
"- این خطا تو یه اپ core تعریف میشه، داخل ماژول exceptions، یعنی چیزی شبیه `project.core.exceptions.ApplicationError`.\n",
"- بقیه موارد رو بذارید DRF به طور پیش‌فرض هندل کنه.\n",
"- `ValidationError` حالا خاصه و قراره متفاوت هندل بشه.\n",
"- `ValidationError` فقط باید از serializer یا اعتبارسنجی مدل بیاد.\n"
],
"id": "815336649f316a13"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " قراره یه ساختار به شکل زیر برای ارور ها به وجود بیاریم",
"id": "4ddd46fff45b17ae"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"The error message here\",\n",
" \"extra\": {}\n",
"}"
],
"id": "9bce28ef60ccd1de"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"\n",
"کلید اضافه (`extra`) می‌تونه هر دیتایی رو نگه داره — هر چیزی که لازم داریم به فرانت منتقل بشه.\n",
"برای مثال، وقتی با یه `ValidationError` سروکار داریم (که معمولاً از یه **Serializer** یا **Model** میاد)،\n",
"می‌خوایم ارور رو به این شکل نمایش بدیم:\n"
],
"id": "b5752591b23367ea"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Validation error.\",\n",
" \"extra\": {\n",
" \"fields\": {\n",
" \"password\": [\"This field cannot be blank.\"],\n",
" \"email\": [\"This field cannot be blank.\"]\n",
" }\n",
" }\n",
"}"
],
"id": "c878c0d2b254c324"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"\n",
"اینجا هدف اینه که فرانت‌اند بتونه بفهمه کدوم فیلدها خطا دارن،\n",
"مثلاً از طریق `extra.fields` تا بتونه ارور هر فیلد رو جدا جدا به کاربر نشون بده.\n",
"\n",
"برای رسیدن به این هدف، یه **exception handler سفارشی** می‌سازیم.\n",
"کدی شبیه به این:"
],
"id": "155b1e5cbaea876f"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied\n",
"from django.http import Http404\n",
"\n",
"from rest_framework.views import exception_handler\n",
"from rest_framework import exceptions\n",
"from rest_framework.serializers import as_serializer_error\n",
"from rest_framework.response import Response\n",
"\n",
"from styleguide_example.core.exceptions import ApplicationError\n",
"\n",
"\n",
"def hacksoft_proposed_exception_handler(exc, ctx):\n",
" \"\"\"\n",
" {\n",
" \"message\": \"Error message\",\n",
" \"extra\": {}\n",
" }\n",
" \"\"\"\n",
" if isinstance(exc, DjangoValidationError):\n",
" exc = exceptions.ValidationError(as_serializer_error(exc))\n",
"\n",
" if isinstance(exc, Http404):\n",
" exc = exceptions.NotFound()\n",
"\n",
" if isinstance(exc, PermissionDenied):\n",
" exc = exceptions.PermissionDenied()\n",
"\n",
" response = exception_handler(exc, ctx)\n",
"\n",
" # If unexpected error occurs (server error, etc.)\n",
" if response is None:\n",
" if isinstance(exc, ApplicationError):\n",
" data = {\n",
" \"message\": exc.message,\n",
" \"extra\": exc.extra\n",
" }\n",
" return Response(data, status=400)\n",
"\n",
" return response\n",
"\n",
" if isinstance(exc.detail, (list, dict)):\n",
" response.data = {\n",
" \"detail\": response.data\n",
" }\n",
"\n",
" if isinstance(exc, exceptions.ValidationError):\n",
" response.data[\"message\"] = \"Validation error\"\n",
" response.data[\"extra\"] = {\n",
" \"fields\": response.data[\"detail\"]\n",
" }\n",
" else:\n",
" response.data[\"message\"] = response.data[\"detail\"]\n",
" response.data[\"extra\"] = {}\n",
"\n",
" del response.data[\"detail\"]\n",
"\n",
" return response"
],
"id": "e71e81f779674049"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " استراتژی ما اینجا استفاده کردن از DRF تا جای ممکن و تغییر اونه",
"id": "6c26c56fdd27247e"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from styleguide_example.core.exceptions import ApplicationError\n",
"\n",
"\n",
"def trigger_application_error():\n",
" raise ApplicationError(message=\"Something is not correct\", extra={\"type\": \"RANDOM\"})"
],
"id": "6f75653b5d35375e"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " جواب :",
"id": "aeb2828ae1397936"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Something is not correct\",\n",
" \"extra\": {\n",
" \"type\": \"RANDOM\"\n",
" }\n",
"}"
],
"id": "998ed2fc11844799"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" raise DjangoValidationError(\"Some error message\")"
],
"id": "61759c185aa72e38"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Validation error\",\n",
" \"extra\": {\n",
" \"fields\": {\n",
" \"non_field_errors\": [\"Some error message\"]\n",
" }\n",
" }\n",
"}"
],
"id": "4c26f5f575fa4dd9"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.exceptions import PermissionDenied\n",
"\n",
"def some_service():\n",
" raise PermissionDenied()"
],
"id": "e7636b8f054e207b"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"You do not have permission to perform this action.\",\n",
" \"extra\": {}\n",
"}"
],
"id": "a6fe417cd1060d6b"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.http import Http404\n",
"\n",
"def some_service():\n",
" raise Http404()"
],
"id": "e6044ff129b213e2"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Not found.\",\n",
" \"extra\": {}\n",
"}"
],
"id": "343ef03b1bfb194e"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" raise RestValidationError(\"Some error message\")"
],
"id": "55956678fb2ff180"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Validation error\",\n",
" \"extra\": {\n",
" \"fields\": [\"Some error message\"]\n",
" }\n",
"}"
],
"id": "58da4ed3ef327982"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" raise RestValidationError(detail={\"error\": \"Some error message\"})"
],
"id": "3d7dedd3453f6249"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Validation error\",\n",
" \"extra\": {\n",
" \"fields\": {\n",
" \"error\": \"Some error message\"\n",
" }\n",
" }\n",
"}"
],
"id": "bbdc9c4e79b9ae0c"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"class NestedSerializer(serializers.Serializer):\n",
" bar = serializers.CharField()\n",
"\n",
"\n",
"class PlainSerializer(serializers.Serializer):\n",
" foo = serializers.CharField()\n",
" email = serializers.EmailField(min_length=200)\n",
"\n",
" nested = NestedSerializer()\n",
"\n",
"\n",
"def some_service():\n",
" serializer = PlainSerializer(data={\n",
" \"email\": \"foo\",\n",
" \"nested\": {}\n",
" })\n",
" serializer.is_valid(raise_exception=True)\n"
],
"id": "748221458f29ffd5"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Validation error\",\n",
" \"extra\": {\n",
" \"fields\": {\n",
" \"foo\": [\"This field is required.\"],\n",
" \"email\": [\n",
" \"Ensure this field has at least 200 characters.\",\n",
" \"Enter a valid email address.\"\n",
" ],\n",
" \"nested\": {\n",
" \"bar\": [\"This field is required.\"]\n",
" }\n",
" }\n",
" }\n",
"}"
],
"id": "8d54fd9e3332fec"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from rest_framework import exceptions\n",
"\n",
"\n",
"def some_service():\n",
" raise exceptions.Throttled()"
],
"id": "b1aa8fb83fd13572"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Request was throttled.\",\n",
" \"extra\": {}\n",
"}"
],
"id": "5d9561f655ffaa91"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"def some_service():\n",
" user = BaseUser()\n",
" user.full_clean()"
],
"id": "1647b7f6c5f81444"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"{\n",
" \"message\": \"Validation error\",\n",
" \"extra\": {\n",
" \"fields\": {\n",
" \"password\": [\"This field cannot be blank.\"],\n",
" \"email\": [\"This field cannot be blank.\"]\n",
" }\n",
" }\n",
"}"
],
"id": "b2b3a68f0ce46d1e"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Celery — مغز پشت صحنه‌ی پروژه\n",
"\n",
"---\n",
"\n",
"## کاربردهای اصلی\n",
"\n",
"سلری رو معمولاً واسه ۳ مدل کار استفاده می‌کنیم:\n",
"\n",
"1. حرف زدن با سرویس‌های خارجی (ایمیل، نوتیف، پیام و از این داستانا)\n",
"2. انداختن کارای سنگین بیرون از چرخه‌ی HTTP (تا یوزر منتظر نمونه)\n",
"3. اجرای کارای زمان‌بندی‌شده با **Celery Beat**\n",
"\n",
"---\n",
"\n",
"## منطق کلی\n",
"\n",
"ما با **Celery** مثل یه رابط رفتار می‌کنیم، نه یه جای منطق اصلی.\n",
"یعنی چی؟ یعنی:\n",
"\n",
"> هیچ کاری که مربوط به منطق پروژه‌ست نباید مستقیم داخل تسک نوشته بشه.\n",
"\n",
"سلری فقط یه مجریه. منطق اصلی باید توی سرویس‌ها (services) نوشته بشه، نه تو تسک‌ها.\n",
"\n",
"\n"
],
"id": "d5e01e664b4c6edb"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.db import transaction\n",
"from django.core.mail import EmailMultiAlternatives\n",
"\n",
"from styleguide_example.core.exceptions import ApplicationError\n",
"from styleguide_example.common.services import model_update\n",
"from styleguide_example.emails.models import Email\n",
"\n",
"\n",
"@transaction.atomic\n",
"def email_send(email: Email) -> Email:\n",
" if email.status != Email.Status.SENDING:\n",
" raise ApplicationError(f\"Cannot send non-ready emails. Current status is {email.status}\")\n",
"\n",
" subject = email.subject\n",
" from_email = \"styleguide-example@hacksoft.io\"\n",
" to = email.to\n",
"\n",
" html = email.html\n",
" plain_text = email.plain_text\n",
"\n",
" msg = EmailMultiAlternatives(subject, plain_text, from_email, [to])\n",
" msg.attach_alternative(html, \"text/html\")\n",
"\n",
" msg.send()\n",
"\n",
" email, _ = model_update(\n",
" instance=email,\n",
" fields=[\"status\", \"sent_at\"],\n",
" data={\n",
" \"status\": Email.Status.SENT,\n",
" \"sent_at\": timezone.now()\n",
" }\n",
" )\n",
" return email"
],
"id": "e3fb09af2d6d3a9b"
},
{
"metadata": {},
"cell_type": "markdown",
"source": " ارسال ایمیل لاجیک بیزینسی داره ولی ما میخوایم این رو با سلری تسک انجام بدیم و یک سرویس رو تریگر کنیم",
"id": "163e071644009da0"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from celery import shared_task\n",
"\n",
"from styleguide_example.emails.models import Email\n",
"\n",
"\n",
"@shared_task\n",
"def email_send(email_id):\n",
" email = Email.objects.get(id=email_id)\n",
"\n",
" from styleguide_example.emails.services import email_send\n",
" email_send(email)"
],
"id": "8c0b1f1d9ca626c5"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"همون‌طور که می‌بینی، ما با **تسک (task)** مثل یه **API** برخورد می‌کنیم:\n",
"\n",
"1. داده‌های لازم رو می‌گیریم.\n",
"2. سرویس مورد نظر رو صدا می‌زنیم.\n",
"\n",
"---\n",
"\n",
"حالا تصور کن یه **سرویس دیگه** داریم که خودش قراره **ارسال ایمیل** رو فعال کنه.\n",
"کدش ممکنه شبیه این باشه:"
],
"id": "9f90a92ea9055870"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.db import transaction\n",
"\n",
"# ... more imports here ...\n",
"\n",
"from styleguide_example.emails.tasks import email_send as email_send_task\n",
"\n",
"\n",
"@transaction.atomic\n",
"def user_complete_onboarding(user: User) -> User:\n",
" # ... some code here\n",
"\n",
" email = email_get_onboarding_template(user=user)\n",
"\n",
" transaction.on_commit(lambda: email_send_task.delay(email.id))\n",
"\n",
" return user"
],
"id": "e723aeab9f78f962"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"\n",
"1. ما تسک (task) رو که **اسمش دقیقاً مثل سرویسه** ایمپورت می‌کنیم،\n",
" ولی یه پسوند `_task` بهش اضافه می‌کنیم تا از هم تفکیک بشن.\n",
"\n",
"2. وقتی **تراکنش (transaction)** تموم می‌شه (commit می‌کنه)،\n",
" اون موقع تسک رو صدا می‌زنیم.\n",
"\n",
"---\n",
"\n",
"### در کل، طرز کار ما با Celery به این شکله:\n",
"\n",
"- **تسک‌ها سرویس‌ها رو صدا می‌زنن.**\n",
"- **سرویس** رو داخل بدنه‌ی تابع تسک ایمپورت می‌کنیم (نه در بالای فایل).\n",
"- **وقتی می‌خوایم یه تسک رو اجرا کنیم،** اون رو در سطح ماژول ایمپورت می‌کنیم و بهش پسوند `_task` می‌دیم.\n",
"- **تسک‌ها رو بعد از commit تراکنش اجرا می‌کنیم،** به‌عنوان یه اثر جانبی از منطق اصلی.\n",
"\n",
"---\n",
"\n",
"### نکته‌ی طراحی\n",
"این ترکیب بین **tasks و services** باعث می‌شه **دایره‌ی ایمپورت‌ها (circular imports)** اتفاق نیفته،\n",
"که یکی از مشکلات رایج موقع استفاده از Celery هست.\n"
],
"id": "576f7039d9619769"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"## Error Handling\n",
"\n",
"گاهی وقتا ممکنه **سرویس ما fail کنه**،\n",
"و بخوایم اون خطا رو در سطح **تسک (task)** هندل کنیم.\n",
"مثلاً بخوایم **تسک رو دوباره retry کنیم**.\n",
"\n",
"اینجور منطق مربوط به خطا **باید داخل خود تسک نوشته بشه**،\n",
"نه داخل سرویس.\n"
],
"id": "706ae47d0958814e"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from celery import shared_task\n",
"from celery.utils.log import get_task_logger\n",
"\n",
"from styleguide_example.emails.models import Email\n",
"\n",
"\n",
"logger = get_task_logger(__name__)\n",
"\n",
"\n",
"def _email_send_failure(self, exc, task_id, args, kwargs, einfo):\n",
" email_id = args[0]\n",
" email = Email.objects.get(id=email_id)\n",
"\n",
" from styleguide_example.emails.services import email_failed\n",
"\n",
" email_failed(email)\n",
"\n",
"\n",
"@shared_task(bind=True, on_failure=_email_send_failure)\n",
"def email_send(self, email_id):\n",
" email = Email.objects.get(id=email_id)\n",
"\n",
" from styleguide_example.emails.services import email_send\n",
"\n",
" try:\n",
" email_send(email)\n",
" except Exception as exc:\n",
" # https://docs.celeryq.dev/en/stable/userguide/tasks.html#retrying\n",
" logger.warning(f\"Exception occurred while sending email: {exc}\")\n",
" self.retry(exc=exc, countdown=5)"
],
"id": "7ae1c321d101abe"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"## Handling Failure\n",
"\n",
"همونطور که می‌بینی، تسک چند بار **retry** می‌کنه،\n",
"ولی اگه **همه‌ی تلاش‌ها fail بشن**، ما این وضعیت رو\n",
"در **callback مخصوص شکست (on_failure)** هندل می‌کنیم.\n",
"\n",
"---\n",
"\n",
"### منطقش اینجوریه:\n",
"اسم callback بر اساس یه الگو ساخته می‌شه\n",
"> `_ {task_name}_failure`\n",
"\n",
"و این تابع هم مثل یه تسک معمولی،\n",
"**سرویس مربوطه رو صدا می‌زنه** (service layer).\n",
"\n",
"---\n",
"\n",
"### مثال ساده:\n",
"\n",
"```python\n",
"from celery import shared_task\n",
"from styleguide_example.emails.models import Email\n",
"\n",
"@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, max_retries=5)\n",
"def email_send(self, email_id):\n",
" try:\n",
" email = Email.objects.get(id=email_id)\n",
" from styleguide_example.emails.services import email_send\n",
" email_send(email)\n",
" except Exception as exc:\n",
" raise self.retry(exc=exc)\n",
"\n",
"\n",
"@shared_task\n",
"def email_send_failure(email_id):\n",
" # اینجا منطق مربوط به fail شدن رو هندل می‌کنیم\n",
" email = Email.objects.get(id=email_id)\n",
" from styleguide_example.emails.services import email_mark_failed\n",
" email_mark_failed(email)\n"
],
"id": "9848bf1f9a58f460"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"## Structure — ساختار تسک‌ها\n",
"\n",
"تسک‌ها معمولا توی **tasks.py** هر اپ قرار می‌گیرن.\n",
"\n",
"قوانین همون قوانین بقیه چیزها هستن (APIs, services, selectors):\n",
"اگه تعداد تسک‌ها برای یه اپ زیاد شد، بهتره اون‌ها رو **بر اساس domain جدا کنیم**.\n",
"\n",
"مثلا:\n",
"- `tasks/domain_a.py`\n",
"- `tasks/domain_b.py`\n",
"\n",
"و کافیه اونا رو توی `tasks/__init__.py` ایمپورت کنیم تا **Celery autodiscover** انجام بده.\n",
"\n",
" نکته‌ی کلی: تسک‌ها رو طوری تقسیم کن که **برای خودت منطقی باشه**.\n",
"\n",
"---\n",
"\n",
"## Periodic Tasks — تسک‌های دوره‌ای\n",
"\n",
"مدیریت تسک‌های دوره‌ای خیلی مهمه، مخصوصا وقتی **ده‌ها یا صدها تسک** داشته باشی.\n",
"\n",
"ما از ترکیب زیر استفاده می‌کنیم:\n",
"- `Celery Beat`\n",
"- `django_celery_beat.schedulers:DatabaseScheduler`\n",
"- `django-celery-beat`\n",
"\n",
"اضافه بر این، یه **management command** داریم به اسم:\n",
"`setup_periodic_tasks`\n",
"\n",
"این کامند **تعریف همه‌ی تسک‌های دوره‌ای سیستم** رو نگه می‌داره و داخل اپ tasks قرار داره.\n",
"\n",
"---\n",
"\n",
"### مسیر فایل مثال:\n",
"`project/tasks/management/commands/setup_periodic_tasks.py`\n"
],
"id": "10a20c6ad31a61fe"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from django.core.management.base import BaseCommand\n",
"from django.db import transaction\n",
"\n",
"from django_celery_beat.models import IntervalSchedule, CrontabSchedule, PeriodicTask\n",
"\n",
"from project.app.tasks import some_periodic_task\n",
"\n",
"\n",
"class Command(BaseCommand):\n",
" help = f\"\"\"\n",
" Setup celery beat periodic tasks.\n",
"\n",
" Following tasks will be created:\n",
"\n",
" - {some_periodic_task.name}\n",
" \"\"\"\n",
"\n",
" @transaction.atomic\n",
" def handle(self, *args, **kwargs):\n",
" print('Deleting all periodic tasks and schedules...\\n')\n",
"\n",
" IntervalSchedule.objects.all().delete()\n",
" CrontabSchedule.objects.all().delete()\n",
" PeriodicTask.objects.all().delete()\n",
"\n",
" periodic_tasks_data = [\n",
" {\n",
" 'task': some_periodic_task\n",
" 'name': 'Do some peridoic stuff',\n",
" # https://crontab.guru/#15_*_*_*_*\n",
" 'cron': {\n",
" 'minute': '15',\n",
" 'hour': '*',\n",
" 'day_of_week': '*',\n",
" 'day_of_month': '*',\n",
" 'month_of_year': '*',\n",
" },\n",
" 'enabled': True\n",
" },\n",
" ]\n",
"\n",
" for periodic_task in periodic_tasks_data:\n",
" print(f'Setting up {periodic_task[\"task\"].name}')\n",
"\n",
" cron = CrontabSchedule.objects.create(\n",
" **periodic_task['cron']\n",
" )\n",
"\n",
" PeriodicTask.objects.create(\n",
" name=periodic_task['name'],\n",
" task=periodic_task['task'].name,\n",
" crontab=cron,\n",
" enabled=periodic_task['enabled']\n",
" )"
],
"id": "e100be6acfe2dfef"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"---\n",
"\n",
"# جمع‌بندی\n",
"\n",
"این استایل‌گاید قرار نیست یه قانون خشک باشه،\n",
"بلکه قراره بهت کمک کنه که **کدهایی بنویسی که خودت ازش لذت ببری.**\n",
"\n",
"دنیا پر از فریم‌ورک‌ها و ابزارهای مختلفه،\n",
"اما در نهایت چیزی که یه پروژه رو ماندگار می‌کنه\n",
"**طرز فکر و نظم ذهنی توسعه‌دهنده‌هاشه.**\n",
"\n",
"اگر این ساختار رو تو پروژه‌هات پیاده کنی،\n",
"هم کار گروهی راحت‌تر میشه، هم رفع باگ‌ها و توسعه‌های بعدی تبدیل میشه به یه بازی\n",
"\n",
"پس اینو به عنوان یه «نقشه راه شخصی» برای جنگو در نظر بگیر —\n",
"یه سبکی که قراره زندگیتو ساده‌تر کنه، نه سخت‌تر.\n",
"\n",
"موفق باشی ایلیا قادری آبان 404\n"
],
"id": "286c22de50c62a62"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}