Easy-to-setup PWA Admin Panel solution based on FastAPI, async SQLAlchemy and pre-render Swelte UI.
PyPI | GitHub | Example App | Frontend
๐ง This is beta-version and frontend part (UI) currenly is in development.
Now available only backend/API part.
- Python 3.11+
- FastAPI and SQLAlclhemy
- Any async DB driver like AsyncPG
- Any ASGI Python server like Uvicorn
- ๐ Easy to integrate - The minimal amount of code that needs to be completed for the integration of the admin panel.
- ๐งฉ Flexible - We can create almost any filters and quires to expand the functionality of the admin panel.
โ๏ธ Fast - NorthAdmin using async DB drivers and Swelte as UI framework to make admin panel as fast as possible.- ๐ฅ PWA - Similar solutions are offered by SSR, which is slower, less convenient and creates additional load on the server.
- ๐ Good-looking - Modern Material UI-based interface thanks to TailWindCSS
Let's assume that we have project with:
- Postgres
- Some SQLAlchemy models
In this example we prefer to use AsyncPG as driver.
๐โ๐จ It is not necessary that your project should use FastAPI as the main framework.
NorthAdmin can be used as separate app.
Poetry:
poetry add sqlalchemy asyncpg uvicorn north_admin
PIP
pip3 install sqlalchemy asyncpg uvicorn north_admin
pip3 freezee -> requirements.txt
For example, this is our User
table look like:
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True, nullable=False, index=True)
password: Mapped[str] = mapped_column(nullable=False)
fullname: Mapped[str] = mapped_column(nullable=True)
is_active: Mapped[bool] = mapped_column(default=False)
is_admin: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[dt] = mapped_column(default=dt.now, server_default=func.current_timestamp())
Suppose we need the ability:
- To view a list of users
- To get detailed information about each of them
- Soft delete (block) them
- We can do it only with non-admin users (User.user_type == UserType.user)
๐ง This is an artificial one-file example.
If you need a more complete production-ready example, see NorthAdmin Test App
So, here an out admin.py
file:
from datetime import datetime as dt
from enum import Enum
from fastapi import FastAPI
from sqlalchemy import bindparam, and_, select, func
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.dialects.postgresql import ENUM
from north_admin import (
NorthAdmin,
FilterGroup,
Filter,
FieldType,
AdminRouter,
AdminMethods,
AuthProvider,
setup_admin,
UserReturnSchema,
)
from north_admin.types import ModelType, QueryType
class Base(DeclarativeBase):
pass
class UserType(str, Enum):
USER = 'user'
ADMIN = 'admin'
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True, nullable=False, index=True)
password: Mapped[str] = mapped_column(nullable=False)
fullname: Mapped[str] = mapped_column(nullable=True)
is_active: Mapped[bool] = mapped_column(default=True)
user_type: Mapped[UserType] = mapped_column(ENUM(UserType, name='user_type'), default=UserType.USER)
created_at: Mapped[dt] = mapped_column(default=dt.now, server_default=func.current_timestamp())
class AdminAuthProvider(AuthProvider):
async def login(
self,
session: AsyncSession,
login: str,
password: str,
) -> User | None:
query = (
select(User)
.filter(User.email == login)
.filter(User.user_type == UserType.ADMIN)
)
return await session.scalar(query)
async def get_user_by_id(
self,
session: AsyncSession,
user_id: int | str,
) -> User | None:
query = (
select(User)
.filter(User.id == user_id)
.filter(User.user_type == UserType.ADMIN)
)
return await session.scalar(query)
async def to_user_scheme(
self,
user: User,
) -> UserReturnSchema:
return UserReturnSchema(
id=user.id,
login=user.email,
fullname=user.fullname,
)
admin_app = NorthAdmin(
sqlalchemy_uri='postgresql+asyncpg://postgres:@127.0.0.1:5432/north_admin_test_app',
jwt_secret_key='JNBjdejjn!w443@wer',
auth_provider=AdminAuthProvider,
)
user_get_columns = [
User.id,
User.email,
User.fullname,
User.user_type,
User.created_at,
]
def exclude_admin_users(query: QueryType) -> QueryType:
query = query.filter(User.user_type != UserType.ADMIN)
return query
admin_app.add_admin_routes(
AdminRouter(
model=User,
model_title='Users',
enabled_methods=[
AdminMethods.GET_ONE,
AdminMethods.GET_LIST,
AdminMethods.SOFT_DELETE,
],
process_query_method=exclude_admin_users,
pkey_column=User.id,
soft_delete_column=User.is_active,
get_columns=user_get_columns,
list_columns=user_get_columns,
filters=[
FilterGroup(
query=(
and_(
User.created_at > dt.now().replace(hour=0, minute=0, second=0),
bindparam('created_today_param'),
)
),
filters=[
Filter(
bindparam='created_today_param',
title='Created today',
field_type=FieldType.BOOLEAN,
),
],
),
FilterGroup(
query=(User.created_at > bindparam('created_after_gt')),
filters=[
Filter(
bindparam='created_after_gt',
title='Created after',
field_type=FieldType.DATETIME,
)
],
),
]
)
)
app = FastAPI()
setup_admin(
admin_app=admin_app,
app=app,
)
๐ง Don't forget to use
Alembic
or other migration tools orDeclarativeBase.Meta.create_all()
to createUser
table in DB
Short Example with psycopg:
Add this in the end of admin.py
:
if __name__ == '__main__':
from sqlalchemy import create_engine
Base.metadata.create_all(
bind=create_engine(
'postgresql+psycopg://postgres:@127.10.0.1:5432/north_admin_test_app'
),
)
Install pscyopg and run it:
poetry add psycopg psycopg-binary
python3 -m admin
Now we finally can run it:
poetry run uvicorn north_admin.test_app:app --host 0.0.0.0 --port 8000 --reload
-
Firstly, we create
AdminAuthProvider
, what must implement three methods -login
,get_user_id
,to_user_scheme
login
- getUser
by login and passwordget_user_by_id
- getUser
by idto_user_schema
- dumpUser
model toUserReturnSchema
.
-
Next we create
NorthAdmin
application with 3 required parameters (sqlalchemy_uri
,jwt_secket_key
andauth_provider
) -
Now we can add admin page to our admin panel (
add_admin_routes
methods)
-
model
- SQLAlchemy model we want to administrate (required) -
model_title
- Verbose title (displayed in UI). By default generate frommodel
-
emoji
- Emoji displayed near to model name in UI. By default - random emoji. -
enabled_methods
- List of actions, awailable in admin panel (GET_LIST
,GET_ONE
,CREATE
,UPDATE
,DELETE
,SOFT_DELETE
). By default - all ot them. -
process_query_method
- Function applied to SQLAlchemy query in (GET_LIST
,GET_ONE
,UPDATE
,DELETE
,SOFT_DELETE
) methods. Feel free to excluding, filtering, etc. -
pkey_column
- Primary Key column. By defaultmodel.id
-
list_columns
- Columns displayed when displaying a list ofmodel
items in UI. By default - all columns. -
get_columns
- Similary for viewing one item. By default - all columns. -
create_columns
- Similary for creatine new item. By default - all non-key columns. -
update_columns
- Similary for updatings existing item. By default - all non-key columns. -
soft_delete_column
- Boolean SQLAlchemy column. When soft deleting (blocking) item, it set toFalse
, is restoring toTrue
. Required, if you haveSOFT_DELETE
inenabled_methods
-
sortable_columns
- List of SQLAclehmy columns, A list of columns to which we can apply sorting in the UI. By default - all key column. (Seesort_by
params in list method) -
filters
- list ofFilterGroup
object. By default no filters applied.
Filters are represented by FilterGroup
and Filter
classes
FilterGroup
contains SQLAlchemy query with some bind_params
.
Value of bind_params
is represented in Filter
title
field and this is exactly the parameter that the front-end will.
Also Filter
object contains name of filter in UI (title
) and UI type (field_type
)
In our case, we will see two filters in the admin panel:
- Checkbox
Created Today
- Date-picker
Created After
Awailable FieldType
:
INTEGER
BOOLEAN
(checkbox)FLOAT
STRING
ENUM
(listpicker)DATETIME
(datetime picker)ARRAY
(several string input)
๐ค Filter system it may seem confusing (overcomplex) at the beginning, but it gives unlimited variability of implemented filters.
Restart FastAPI application and admin panel is ready.
E.q. we running app with Uvicorn on http://127.0.0.1 we have:
- Admin Panel UI in http://127.0.0.1/admin/
- Admin Panel API in http://127.0.0.1/admin/api/
- Swagger in http://127.0.0.1/admin/docs/
Create user with user_type=UserType.ROOT
, check out Swagger and enjoy using your new admin panel.