تمرين المرحلة الأولى الشامل — بناء PriceTracker API الأساسي
المفاهيم المغطاة في الدروس الخمسة للمرحلة الأولى كقطع أحجية. حان الوقت لتجميعها معاً لتشكيل صورة كاملة — PriceTracker API الأساسي — بحيث يمكن لواجهة Bob الأمامية استهلاك البيانات فعلياً.
1. ما ستتعلمه
- تصميم وتنفيذ ثلاث مجموعات من نقاط النهاية الأساسية:
/productsو/products/{id}و/prices - نظام نماذج Pydantic V2 الكامل:
ProductCreate/ProductResponse/PriceCreate/PriceResponse - أمثلة عملية على الجمع بين معاملات المسار ومعاملات الاستعلام وهياكل الطلب
- استخدام
response_modelلتنفيذ منطق الأعمال "للاستعلام العام مع إخفاء أسعار الجملة" - Bob (تكامل الواجهة الأمامية): تأكيد أن توثيق OpenAPI يمكن توليده تلقائياً لاستخدامه من قبل SDK الواجهة الأمامية
2. القصة الحقيقية لـ Alice
(1) نقطة الألم: المعارف المبعثرة لا يمكن تجميعها في منتج
أنهت Alice الدروس الخمسة الأولى، لكن الأمثلة في كل درس هي مقتطفات مستقلة — أمثلة معاملات المسار وPydantic غير متصلة. لم يعد Bob يتحمل: "أحتاج API يعمل، وليس مجموعة من العروض المبعثرة!" تحتاج Alice إلى دمج تصميم التوجيه والتحقق من المعاملات والتحقق من البيانات وتصفية الاستجابة في خدمة API واحدة وظيفية.
(2) حلول شاملة للمشاكل الواقعية
يدمج هذا الدرس جميع المفاهيم المغطاة في الدروس الخمسة الأولى في PriceTracker API الأساسي، الذي يتضمن مجموعة كاملة من نقاط نهاية CRUD ونظام نماذج Pydantic وسلسلة تحقق من المعاملات ومنطق تصفية الاستجابة.
# بنية 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) تخطيط نقاط نهاية المرحلة الأولى
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 الكامل
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
الناتج:
# تم تعريف الدالة بنجاح
4. دورة حياة الطلب-الاستجابة
(1) سير العمل الكامل
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 للمنتجات
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]
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶ مثال: نقاط نهاية التحديث والحذف
@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"}
الناتج:
# تم تعريف الدالة بنجاح
5. نقاط نهاية الأسعار واستراتيجيات الجمع في الممارسة
(1) الجمع بين معاملات الاستعلام وهيكل الطلب
(1) ▶ مثال: نقاط نهاية استعلام وإنشاء الأسعار
@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
الناتج:
# تم تعريف الدالة بنجاح
6. دليل عملي لتصفية الاستجابة: النسخة العامة مقابل نسخة المشرف
(1) سيناريو الأعمال: إخفاء أسعار الجملة
يمكن لمستخدمي التجزئة في PriceTracker عرض أسعار التجزئة فقط، بينما يمكن للمشرفين عرض أسعار الجملة والتكلفة.
(1) ▶ مثال: نموذج استجابة متعدد الأدوار
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]
الناتج:
# تم تعريف الدالة بنجاح
❓أسئلة شائعة
ProductCreate وProductResponse؟Create لا يتضمن معرّفاً (يُولَّد على الخادم)، بينما نموذج Response يتضمن معرّفاً. هذه أيضاً أفضل ممارسة أمنية — لا يجب أن يحدد العميل المعرّف.exclude_unset=True في نقطة نهاية PUT؟None. بالاقتران مع الحقول الاختيارية في ProductUpdate، هذا يتيح التحديثات الجزئية./openapi.json، واستخدم npx openapi-typescript لتوليد أنواع TypeScript، أو استخدم Swagger Codegen لتوليد SDK.ProductBase(BaseModel)، واجعل ProductCreate(ProductBase) وProductResponse(ProductBase) يرثان ويمتدانها.📖ملخص
- المرحلة الأولى: اكتملت ثلاث مجموعات من نقاط النهاية الأساسية لـ PriceTracker: CRUD المنتجات، استعلام/إنشاء الأسعار، والعرض العام/الإدارة
- بنية نماذج Pydantic: ثلاث طبقات متميزة — Create (تحقق الإدخال)، Update (تحديثات جزئية)، Response (تصفية المخرجات)
- معاملات المسار ومعاملات الاستعلام وهيكل الطلب تتجمع بشكل طبيعي؛ يميزها FastAPI تلقائياً حسب النوع
response_modelيحقق "نفس البيانات، عروض مختلفة" — النسخة العامة تخفي أسعار الجملة، بينما نسخة المشرف تعرض سلسلة الأسعار الكاملة- توليد تلقائي لتوثيق OpenAPI؛ يمكن لمطوري الواجهة الأمامية استخدام Swagger UI للاختبار أو توليد SDKs مباشرة
📝تمارين
- تمرين أساسي (الصعوبة ⭐): ادمج جميع الكود من هذا الدرس في ملف
app/main.pyواحد، ابدأ الخدمة، واستخدم Swagger UI لإنشاء ثلاثة منتجات والاستعلام عن القائمة. تلميح:uvicorn app.main:app --reload - تمرين متقدم (الصعوبة ⭐⭐): أضف نقطة نهاية
/products/{id}/pricesللاستعلام عن جميع سجلات الأسعار لمنتج محدد، مع دعم معاملات الاستعلامsort(date/price) وlimit. تلميح: معامل مسارproduct_id+ معاملات استعلامsort/limit - تحدٍ (الصعوبة: ⭐⭐⭐): نفّذ نظام عرض مزدوج باستخدام
ProductAdminوProductPublic— أنشئ بيانات منتج تحتوي على سلسلة الأسعار الكاملة. يجب أن تُرجع نقطة النهاية العامة سعر التجزئة فقط، بينما تُرجع نقطة نهاية المشرف سعر التجزئة وسعر الجملة وهامش الربح. تلميح: نموذجاresponse_model+ نفس مصدر البيانات
---|



