Tezkor boshlash

Bo‘sh papkadan sahifada haqiqiy model bilan kirilgan admin panelgacha — taxminan o‘n besh daqiqada.

Mavjud v0.1.0

Bularning barchasi hech narsani ishga tushirishni talab qilmaydigan SQLite’da boshidan oxirigacha bajariladi. Oxirida PostgreSQL’ga o‘tish bitta qator bilan amalga oshadi.

1. O‘rnatish

mkdir shop && cd shop
uv init
uv add "fastfort[sqlalchemy,sqlite,cli]"

ORM’lar alohida extra sifatida ulanadi va ularning hech biri paket darajasida import qilinmaydi, shuning uchun birini o‘rnatish ikkinchisini hech qachon ortidan tortib kelmaydi.

2. Loyihangizga tegishli ma’lumotlar bazasi

FastFort hech qachon o‘zining ulanishini ochmaydi. U sizning allaqachon qurgan sessiya fabrikangizni oladi.

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


engine = create_async_engine("sqlite+aiosqlite:///./shop.db")

# expire_on_commit=False is not a preference. The admin's views read attributes
# off an object after the unit of work has committed — the label in a success
# message, the primary key to redirect to. With the default, each of those is a
# synchronous refresh against an async session, which raises MissingGreenlet.
session_factory: async_sessionmaker[AsyncSession] = async_sessionmaker(
    engine, expire_on_commit=False
)

3. Modellar

Oddiy deklarativ modellar. Bu yerda meros olish uchun FastFort bazaviy klassi yo‘q — aynan shu narsa mavjud sxemaga hech qanday migratsiyasiz admin panel olish imkonini beradi.

import datetime as dt
from decimal import Decimal

import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column, relationship

from .db import Base


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(sa.String(255), unique=True)
    full_name: Mapped[str | None] = mapped_column(sa.String(160), default=None)
    hashed_password: Mapped[str] = mapped_column(sa.String(255), default="")
    is_active: Mapped[bool] = mapped_column(default=True)
    is_staff: Mapped[bool] = mapped_column(default=False)
    is_superuser: Mapped[bool] = mapped_column(default=False)
    created_at: Mapped[dt.datetime] = mapped_column(
        sa.DateTime(timezone=True), default=lambda: dt.datetime.now(dt.UTC)
    )

    # Every foreign-key cell, every picker option and every delete confirmation
    # renders this. Without it they all show `<User object at 0x…>`.
    def __str__(self) -> str:
        return self.email


class Category(Base):
    __tablename__ = "categories"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(sa.String(120), unique=True)

    def __str__(self) -> str:
        return self.name


class Product(Base):
    __tablename__ = "products"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(sa.String(200))
    description: Mapped[str | None] = mapped_column(sa.Text(), default=None)
    price: Mapped[Decimal] = mapped_column(sa.Numeric(12, 2), default=Decimal("0.00"))
    stock: Mapped[int] = mapped_column(default=0)
    is_active: Mapped[bool] = mapped_column(default=True)
    created_at: Mapped[dt.datetime] = mapped_column(
        sa.DateTime(timezone=True), default=lambda: dt.datetime.now(dt.UTC)
    )

    category_id: Mapped[int | None] = mapped_column(
        sa.ForeignKey("categories.id"), default=None
    )
    category: Mapped[Category | None] = relationship()

    def __str__(self) -> str:
        return self.name

Userdagi beshta odatiy atribut nomi — identifikator, parol, faollik, xodim, superfoydalanuvchi — avtomatik aniqlanadi, shuning uchun set_user_model hech qanday argument talab qilmaydi. Ularni boshqacha nomlasangiz, o‘zingiz aniq ko‘rsatib berasiz.

4. Admin panelda ro‘yxatdan o‘tkazish

@admin.register import vaqtida ishlaydi va ro‘yxatdan o‘tkazishni bufferlaydi — aynan shu sababli bu modul faqat modellarni import qiladi, boshqa hech narsani emas. Ilovani import qiladigan admin moduli har qanday loyihada aylanma import bo‘lib qolardi.

from fastfort import admin

from .models import Category, Product, User


@admin.register(User, key="accounts.user")
class UserAdmin(admin.ModelAdmin):
    list_display = ("id", "email", "full_name", "is_active", "is_staff", "created_at")
    list_filter = ("is_active", "is_staff", "is_superuser")
    search_fields = ("email", "full_name")
    ordering = ("-created_at",)
    readonly_fields = ("created_at",)
    group_name = "Accounts"
    icon = "users"


@admin.register(Category, key="shop.category")
class CategoryAdmin(admin.ModelAdmin):
    list_display = ("id", "name")
    search_fields = ("name",)
    ordering = ("name",)
    group_name = "Shop"
    icon = "folder"


@admin.register(Product, key="shop.product")
class ProductAdmin(admin.ModelAdmin):
    list_display = ("id", "name", "category", "price", "stock", "is_active")
    list_filter = ("is_active", "category", "price")
    search_fields = ("name", "description")
    ordering = ("-created_at",)
    # Without this, rendering the category column costs one query per row.
    select_related = ("category",)
    readonly_fields = ("created_at",)
    group_name = "Shop"
    icon = "box"

    actions = ("delete", "archive")

    @admin.action("Archive", icon="box")
    async def archive(self, adapter, objects):
        for product in objects:
            await adapter.update(product, {"is_active": False})
        return f"{len(objects)} products archived."

5. Ulash

from contextlib import asynccontextmanager

from fastapi import FastAPI

from fastfort import FastFort, FastFortSettings
from fastfort.orm.sqlalchemy import SQLAlchemyBackend

from app.db import Base, engine, session_factory
from app.models import User

fort = FastFort(
    settings=FastFortSettings(project_name="Shop"),
    backend=SQLAlchemyBackend(session_factory=session_factory, base=Base),
)
fort.set_user_model(User, identity_field="email")

# Imports app/admin.py so its decorators run. `fort.autodiscover("app")` is the
# alternative: it walks the package for every `admin.py` it can find.
fort.include_admin("app.admin")


@asynccontextmanager
async def lifespan(_: FastAPI):
    async with engine.begin() as connection:
        await connection.run_sync(Base.metadata.create_all)
    yield
    await engine.dispose()


app = FastAPI(lifespan=lifespan)
fort.mount(app)

FastFortSettingsda standart secret_key yo‘q, va bu ataylab shunday: standart qiymatni o‘zida olib yuradigan freymvork qandaydir joylashtirishning aynan shu qiymat bilan ishlashini kafolatlaydi. O‘zingiznikini generatsiya qiling va muhit o‘zgaruvchisiga joylashtiring.

6. Kalit, hisob, server

export FASTFORT_SECRET_KEY=$(uv run fastfort generate-secret)

FF_PASSWORD='choose-something-real' uv run fastfort createsuperuser \
    --app main:fort \
    --identity you@example.com \
    --password-env FF_PASSWORD \
    --no-input

uv run uvicorn main:app --reload

--password-env, --password emas: argument sifatida uzatilgan parol buyruqlar tarixida va ps chiqishida ko‘rinib turadi.

http://127.0.0.1:8000/admin manzilini oching va tizimga kiring.

7. Sozlamalarni tekshirish

uv run fastfort check --app main:fort --deploy

U birinchi muammoda to‘xtash o‘rniga barcha muammolarni to‘playdi va nolga teng bo‘lmagan kod bilan yakunlanadi — shu bilan joylashtirishni to‘xtatib turishi mumkin:

Shop · 3 model(s)
warn  security.cookie_secure=False sends the session cookie over plain HTTP.
      Set FASTFORT_SECURITY__COOKIE_SECURE=true and serve over HTTPS.
warn  debug=True exposes tracebacks and internal state. Set FASTFORT_DEBUG=false.

2 problem(s) found.

PostgreSQL’ga o‘tish

Bitta qator, ortiga extra bilan:

uv add "fastfort[postgres]"
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/shop")

ORM qatlamidan yuqorida hech narsa o‘zgarmaydi. Dvijoklar orasida farq qiladigan har qanday narsa backend’ning dialekt modulida yashaydi — u aynan «uchalasida ham ishlaydi» degan da’voni test bilan tekshirib bo‘lish uchun mavjud.

Keyingi qadam