404 Not Found

404 Not Found


nginx

تمرين المرحلة الأولى الشامل — بناء PriceTracker API الأساسي

المفاهيم المغطاة في الدروس الخمسة للمرحلة الأولى كقطع أحجية. حان الوقت لتجميعها معاً لتشكيل صورة كاملة — PriceTracker API الأساسي — بحيث يمكن لواجهة Bob الأمامية استهلاك البيانات فعلياً.

1. ما ستتعلمه


2. القصة الحقيقية لـ Alice

(1) نقطة الألم: المعارف المبعثرة لا يمكن تجميعها في منتج

أنهت Alice الدروس الخمسة الأولى، لكن الأمثلة في كل درس هي مقتطفات مستقلة — أمثلة معاملات المسار وPydantic غير متصلة. لم يعد Bob يتحمل: "أحتاج API يعمل، وليس مجموعة من العروض المبعثرة!" تحتاج Alice إلى دمج تصميم التوجيه والتحقق من المعاملات والتحقق من البيانات وتصفية الاستجابة في خدمة API واحدة وظيفية.

(2) حلول شاملة للمشاكل الواقعية

يدمج هذا الدرس جميع المفاهيم المغطاة في الدروس الخمسة الأولى في PriceTracker API الأساسي، الذي يتضمن مجموعة كاملة من نقاط نهاية CRUD ونظام نماذج Pydantic وسلسلة تحقق من المعاملات ومنطق تصفية الاستجابة.

PYTHON
# بنية PriceTracker API الأساسية الكاملة
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="PriceTracker API", version="0.1.0")

# نماذج، مسارات، تحقق — كلها مدمجة

(3) العائد

أصبح لدى Alice خدمة API تعمل، ويمكن لـ Bob استخدام Swagger UI لاختبار جميع نقاط النهاية؛ التوثيق المُولَّد تلقائياً لـ OpenAPI يسمح لـ SDKs الواجهة الأمامية باستهلاك API مباشرة.


3. التصريف الشامل لنقاط نهاية API

(1) تخطيط نقاط نهاية المرحلة الأولى

100%
flowchart LR
    Client[العميل] -->|GET /products| List[قائمة المنتجات]
    Client -->|POST /products| Create[إنشاء منتج]
    Client -->|GET /products/id| Detail[تفاصيل المنتج]
    Client -->|PUT /products/id| Update[تحديث منتج]
    Client -->|DELETE /products/id| Delete[حذف منتج]
    Client -->|GET /prices| Search[بحث الأسعار]
    Client -->|POST /prices| AddPrice[إضافة سعر]
    
    List --> PM[ProductResponse]
    Create --> PM
    Detail --> PDP[ProductDetailResponse]
    Search --> PRM[PriceResponse]
    AddPrice --> PRM
نقطة النهاية الطريقة معاملات المسار معاملات الاستعلام هيكل الطلب نموذج الاستجابة
قائمة المنتجات GET - category، sort، limit، offset - list[ProductResponse]
إنشاء منتج POST - - ProductCreate ProductResponse
تفاصيل المنتج GET product_id - - ProductDetailResponse
تحديث منتج PUT product_id - ProductUpdate ProductResponse
حذف منتج DELETE product_id - - dict
استعلام الأسعار GET - product_id، min_price، max_price - list[PriceResponse]
إضافة سعر POST - - PriceCreate PriceResponse

(1) ▶ مثال: نظام نماذج Pydantic الكامل

PYTHON
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional
from enum import Enum
from datetime import datetime

class Category(str, Enum):
    electronics = "electronics"
    clothing = "clothing"
    food = "food"
    books = "books"

class PriceInfo(BaseModel):
    amount: float = Field(gt=0, description="مبلغ السعر بالدولار")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: Category
    base_price: float = Field(gt=0, description="السعر الأساسي بالدولار")
    description: Optional[str] = Field(None, max_length=2000)

class ProductUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=200)
    category: Optional[Category] = None
    base_price: Optional[float] = Field(None, gt=0)
    description: Optional[str] = Field(None, max_length=2000)

class ProductResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    category: str
    base_price: float

class ProductDetailResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    category: str
    base_price: float
    description: Optional[str] = None
    created_at: datetime

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0, description="السعر بالدولار")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
    source: str = Field(max_length=100)

    @field_validator("price")
    @classmethod
    def round_price(cls, v: float) -> float:
        return round(v, 2)

class PriceResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    product_id: int
    price: float
    currency: str
    source: str
    recorded_at: datetime

الناتج:

TEXT
# تم تعريف الدالة بنجاح

4. دورة حياة الطلب-الاستجابة

(1) سير العمل الكامل

100%
sequenceDiagram
    participant Client as واجهة Bob الأمامية
    participant FastAPI as موجه FastAPI
    participant Pydantic as مُحقِّق Pydantic
    participant Handler as دالة العرض
    participant DB as قاعدة بيانات في الذاكرة

    Client->>FastAPI: POST /products (هيكل JSON)
    FastAPI->>Pydantic: تحقق باستخدام ProductCreate
    Pydantic-->>FastAPI: نسخة نموذج مُتحقَّق منها
    FastAPI->>Handler: create_product(data: ProductCreate)
    Handler->>DB: تخزين المنتج
    DB-->>Handler: سجل مخزّن
    Handler-->>FastAPI: إرجاع القاموس الكامل
    FastAPI->>Pydantic: تصفية باستخدام ProductResponse
    Pydantic-->>Client: استجابة JSON (مُصفَّاة)

(1) ▶ مثال: تنفيذ نقاط نهاية CRUD للمنتجات

PYTHON
from fastapi import FastAPI, Path, Query, HTTPException
from datetime import datetime

app = FastAPI(title="PriceTracker API", version="0.1.0")

# تخزين في الذاكرة (سيُستبدل بقاعدة بيانات في المرحلة الثانية)
products_db: dict[int, dict] = {}
prices_db: list[dict] = []
_product_counter = 0
_price_counter = 0

@app.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
    global _product_counter
    _product_counter += 1
    record = {
        "id": _product_counter,
        "name": product.name,
        "category": product.category.value,
        "base_price": product.base_price,
        "description": product.description,
        "created_at": datetime.utcnow(),
    }
    products_db[_product_counter] = record
    return record

@app.get("/products", response_model=list[ProductResponse])
async def list_products(
    category: Optional[Category] = Query(None),
    sort: str = Query("name", pattern="^(name|base_price)$"),
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    items = list(products_db.values())
    if category:
        items = [p for p in items if p["category"] == category.value]
    items.sort(key=lambda p: p.get(sort, ""))
    return items[offset : offset + limit]

@app.get("/products/{product_id}", response_model=ProductDetailResponse)
async def get_product(
    product_id: int = Path(gt=0, description="معرّف المنتج"),
):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return products_db[product_id]

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: نقاط نهاية التحديث والحذف

PYTHON
@app.put("/products/{product_id}", response_model=ProductResponse)
async def update_product(
    product_id: int = Path(gt=0),
    update: ProductUpdate = ...,
):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    record = products_db[product_id]
    update_data = update.model_dump(exclude_unset=True)
    record.update(update_data)
    return record

@app.delete("/products/{product_id}")
async def delete_product(product_id: int = Path(gt=0)):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    del products_db[product_id]
    return {"message": "Product deleted"}

الناتج:

TEXT
# تم تعريف الدالة بنجاح

5. نقاط نهاية الأسعار واستراتيجيات الجمع في الممارسة

(1) الجمع بين معاملات الاستعلام وهيكل الطلب

(1) ▶ مثال: نقاط نهاية استعلام وإنشاء الأسعار

PYTHON
@app.get("/prices", response_model=list[PriceResponse])
async def search_prices(
    product_id: Optional[int] = Query(None, gt=0),
    min_price: float = Query(0, ge=0, description="الحد الأدنى للسعر بالدولار"),
    max_price: float = Query(999999, ge=0, description="الحد الأقصى للسعر بالدولار"),
    limit: int = Query(50, ge=1, le=200),
    offset: int = Query(0, ge=0),
):
    results = prices_db
    if product_id:
        results = [p for p in results if p["product_id"] == product_id]
    results = [p for p in results if min_price <= p["price"] <= max_price]
    return results[offset : offset + limit]

@app.post("/prices", response_model=PriceResponse, status_code=201)
async def create_price(price: PriceCreate):
    global _price_counter
    if price.product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    _price_counter += 1
    record = {
        "id": _price_counter,
        "product_id": price.product_id,
        "price": price.price,
        "currency": price.currency,
        "source": price.source,
        "recorded_at": datetime.utcnow(),
    }
    prices_db.append(record)
    return record

الناتج:

TEXT
# تم تعريف الدالة بنجاح

6. دليل عملي لتصفية الاستجابة: النسخة العامة مقابل نسخة المشرف

(1) سيناريو الأعمال: إخفاء أسعار الجملة

يمكن لمستخدمي التجزئة في PriceTracker عرض أسعار التجزئة فقط، بينما يمكن للمشرفين عرض أسعار الجملة والتكلفة.

(1) ▶ مثال: نموذج استجابة متعدد الأدوار

PYTHON
class ProductPublicResponse(BaseModel):
    """عرض عام - إخفاء أسعار الجملة والتكلفة"""
    id: int
    name: str
    category: str
    retail_price: float

class ProductAdminResponse(BaseModel):
    """عرض المشرف - عرض سلسلة الأسعار الكاملة"""
    id: int
    name: str
    category: str
    retail_price: float
    wholesale_price: float
    cost_price: float
    margin_pct: float  # نسبة هامش الربح

# تخزين في الذاكرة ممتد بمستويات التسعير
admin_products_db: dict[int, dict] = {}

@app.get("/products/{product_id}/public", response_model=ProductPublicResponse)
async def get_product_public(product_id: int = Path(gt=0)):
    if product_id not in admin_products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return admin_products_db[product_id]

@app.get("/products/{product_id}/admin", response_model=ProductAdminResponse)
async def get_product_admin(product_id: int = Path(gt=0)):
    if product_id not in admin_products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return admin_products_db[product_id]

الناتج:

TEXT
# تم تعريف الدالة بنجاح

❓أسئلة شائعة

س ماذا أفعل إذا فُقدت البيانات من التخزين في الذاكرة بعد إعادة التشغيل؟
ج المرحلة الأولى تستخدم التخزين في الذاكرة لتبسيط التعلم؛ في المرحلة الثانية، سيُستبدل بـ PostgreSQL + SQLAlchemy. هذه استراتيجية تعلم تدرجية.
س لماذا تم فصل ProductCreate وProductResponse؟
ج فصل المسؤوليات: نموذج Create لا يتضمن معرّفاً (يُولَّد على الخادم)، بينما نموذج Response يتضمن معرّفاً. هذه أيضاً أفضل ممارسة أمنية — لا يجب أن يحدد العميل المعرّف.
س ما غرض exclude_unset=True في نقطة نهاية PUT؟
ج يسمح للعميل بتحديث فقط الحقول التي يريد تغييرها؛ الحقول غير المقدمة لا تُستبدل وتبقى على None. بالاقتران مع الحقول الاختيارية في ProductUpdate، هذا يتيح التحديثات الجزئية.
س كيف أختبر ما إذا كان توثيق OpenAPI يمكن استهلاكه من قبل تطبيق واجهة أمامية؟
ج زُر /openapi.json، واستخدم npx openapi-typescript لتوليد أنواع TypeScript، أو استخدم Swagger Codegen لتوليد SDK.
س كيف تُعرض قيم معاملات التعداد في التوثيق؟
ج يعرض FastAPI تلقائياً جميع قيم التعداد في القائمة المنسدلة في Swagger UI، مما يجعلها واضحة فوراً لـ Bob مطور الواجهة الأمامية.
س كيف يمكن تجنب التكرار عندما تشترك نقاط نهاية متعددة في نموذج؟
ج استخدم وراثة النماذج: عرّف الحقول المشتركة في ProductBase(BaseModel)، واجعل ProductCreate(ProductBase) وProductResponse(ProductBase) يرثان ويمتدانها.

📖ملخص


📝تمارين

  1. تمرين أساسي (الصعوبة ⭐): ادمج جميع الكود من هذا الدرس في ملف app/main.py واحد، ابدأ الخدمة، واستخدم Swagger UI لإنشاء ثلاثة منتجات والاستعلام عن القائمة. تلميح: uvicorn app.main:app --reload
  2. تمرين متقدم (الصعوبة ⭐⭐): أضف نقطة نهاية /products/{id}/prices للاستعلام عن جميع سجلات الأسعار لمنتج محدد، مع دعم معاملات الاستعلام sort (date/price) وlimit. تلميح: معامل مسار product_id + معاملات استعلام sort/limit
  3. تحدٍ (الصعوبة: ⭐⭐⭐): نفّذ نظام عرض مزدوج باستخدام ProductAdmin وProductPublic — أنشئ بيانات منتج تحتوي على سلسلة الأسعار الكاملة. يجب أن تُرجع نقطة النهاية العامة سعر التجزئة فقط، بينما تُرجع نقطة نهاية المشرف سعر التجزئة وسعر الجملة وهامش الربح. تلميح: نموذجا response_model + نفس مصدر البيانات

---|

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%