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
- Model admini — har bir opsiya va u nimani o‘zgartiradi.
- Sozlamalar — butun sozlash imkoniyatlari.