Skip to content

Commit 832014b

Browse files
committed
migrated to sqlmodel
1 parent bedf645 commit 832014b

28 files changed

+222
-347
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
- [`Docker Compose`](https://docs.docker.com/compose/) With a single command, create and start all the services from your configuration.
4343
- [`NGINX`](https://nginx.org/en/) High-performance low resource consumption web server used for Reverse Proxy and Load Balancing.
4444

45+
> \[!TIP\]
46+
> If you want the `SQLAlchemy + Pydantic` version instead, head to [Fastapi-boilerplate](https://github.com/igorbenav/FastAPI-boilerplate).
47+
4548
## 1. Features
4649

4750
- ⚡️ Fully async

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ arq = "^0.25.0"
3131
gunicorn = "^22.0.0"
3232
bcrypt = "^4.1.1"
3333
fastcrud = "^0.12.0"
34+
sqlmodel = "^0.0.18"
3435

3536

3637
[build-system]

src/app/api/dependencies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ..crud.crud_tier import crud_tiers
1414
from ..crud.crud_users import crud_users
1515
from ..models.user import User
16-
from ..schemas.rate_limit import sanitize_path
16+
from ..models.rate_limit import sanitize_path
1717

1818
logger = logging.getLogger(__name__)
1919

src/app/api/v1/posts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from ...core.utils.cache import cache
1111
from ...crud.crud_posts import crud_posts
1212
from ...crud.crud_users import crud_users
13-
from ...schemas.post import PostCreate, PostCreateInternal, PostRead, PostUpdate
14-
from ...schemas.user import UserRead
13+
from ...models.post import PostCreate, PostCreateInternal, PostRead, PostUpdate
14+
from ...models.user import UserRead
1515

1616
router = APIRouter(tags=["posts"])
1717

src/app/api/v1/rate_limits.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ...core.exceptions.http_exceptions import DuplicateValueException, NotFoundException, RateLimitException
1010
from ...crud.crud_rate_limit import crud_rate_limits
1111
from ...crud.crud_tier import crud_tiers
12-
from ...schemas.rate_limit import RateLimitCreate, RateLimitCreateInternal, RateLimitRead, RateLimitUpdate
12+
from ...models.rate_limit import RateLimitCreate, RateLimitCreateInternal, RateLimitRead, RateLimitUpdate
1313

1414
router = APIRouter(tags=["rate_limits"])
1515

src/app/api/v1/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ...api.dependencies import rate_limiter
77
from ...core.utils import queue
8-
from ...schemas.job import Job
8+
from ...models.job import Job
99

1010
router = APIRouter(prefix="/tasks", tags=["tasks"])
1111

src/app/api/v1/tiers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ...core.db.database import async_get_db
99
from ...core.exceptions.http_exceptions import DuplicateValueException, NotFoundException
1010
from ...crud.crud_tier import crud_tiers
11-
from ...schemas.tier import TierCreate, TierCreateInternal, TierRead, TierUpdate
11+
from ...models.tier import TierCreate, TierCreateInternal, TierRead, TierUpdate
1212

1313
router = APIRouter(tags=["tiers"])
1414

src/app/api/v1/users.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
from ...crud.crud_rate_limit import crud_rate_limits
1212
from ...crud.crud_tier import crud_tiers
1313
from ...crud.crud_users import crud_users
14-
from ...models.tier import Tier
15-
from ...schemas.tier import TierRead
16-
from ...schemas.user import UserCreate, UserCreateInternal, UserRead, UserTierUpdate, UserUpdate
14+
from ...models.tier import Tier, TierRead
15+
from ...models.user import UserCreate, UserCreateInternal, UserRead, UserTierUpdate, UserUpdate
1716

1817
router = APIRouter(tags=["users"])
1918

src/app/core/db/database.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
from sqlalchemy.ext.asyncio import create_async_engine
22
from sqlalchemy.ext.asyncio.session import AsyncSession
3-
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, sessionmaker
3+
from sqlalchemy.orm import sessionmaker
44

55
from ..config import settings
66

77

8-
class Base(DeclarativeBase, MappedAsDataclass):
9-
pass
10-
11-
128
DATABASE_URI = settings.POSTGRES_URI
139
DATABASE_PREFIX = settings.POSTGRES_ASYNC_PREFIX
1410
DATABASE_URL = f"{DATABASE_PREFIX}{DATABASE_URI}"

src/app/core/db/token_blacklist.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
from datetime import datetime
2+
from sqlmodel import SQLModel, Field
23

3-
from sqlalchemy import DateTime, String
4-
from sqlalchemy.orm import Mapped, mapped_column
5-
6-
from .database import Base
7-
8-
9-
class TokenBlacklist(Base):
10-
__tablename__ = "token_blacklist"
11-
12-
id: Mapped[int] = mapped_column("id", autoincrement=True, nullable=False, unique=True, primary_key=True, init=False)
13-
token: Mapped[str] = mapped_column(String, unique=True, index=True)
14-
expires_at: Mapped[datetime] = mapped_column(DateTime)
4+
class TokenBlacklist(SQLModel, table=True):
5+
id: int = Field(default=None, primary_key=True, nullable=False)
6+
token: str = Field(index=True, nullable=False, unique=True)
7+
expires_at: datetime = Field(nullable=False)

src/app/core/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from fastapi import APIRouter, Depends, FastAPI
1111
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
1212
from fastapi.openapi.utils import get_openapi
13+
from sqlmodel import SQLModel
1314

1415
from ..api.dependencies import get_current_superuser
1516
from ..middleware.client_cache_middleware import ClientCacheMiddleware
@@ -24,15 +25,14 @@
2425
RedisRateLimiterSettings,
2526
settings,
2627
)
27-
from .db.database import Base
2828
from .db.database import async_engine as engine
2929
from .utils import cache, queue, rate_limit
3030

3131

3232
# -------------- database --------------
3333
async def create_tables() -> None:
3434
async with engine.begin() as conn:
35-
await conn.run_sync(Base.metadata.create_all)
35+
await conn.run_sync(SQLModel.metadata.create_all)
3636

3737

3838
# -------------- cache --------------

src/app/core/utils/rate_limit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from sqlalchemy.ext.asyncio import AsyncSession
55

66
from ...core.logger import logging
7-
from ...schemas.rate_limit import sanitize_path
7+
from ...models.rate_limit import sanitize_path
88

99
logger = logging.getLogger(__name__)
1010

src/app/crud/crud_posts.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from fastcrud import FastCRUD
22

3-
from ..models.post import Post
4-
from ..schemas.post import PostCreateInternal, PostDelete, PostUpdate, PostUpdateInternal
3+
from ..models.post import Post, PostCreateInternal, PostDelete, PostUpdate, PostUpdateInternal
54

65
CRUDPost = FastCRUD[Post, PostCreateInternal, PostUpdate, PostUpdateInternal, PostDelete]
76
crud_posts = CRUDPost(Post)

src/app/crud/crud_rate_limit.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from fastcrud import FastCRUD
22

3-
from ..models.rate_limit import RateLimit
4-
from ..schemas.rate_limit import RateLimitCreateInternal, RateLimitDelete, RateLimitUpdate, RateLimitUpdateInternal
3+
from ..models.rate_limit import RateLimit, RateLimitCreateInternal, RateLimitDelete, RateLimitUpdate, RateLimitUpdateInternal
54

65
CRUDRateLimit = FastCRUD[RateLimit, RateLimitCreateInternal, RateLimitUpdate, RateLimitUpdateInternal, RateLimitDelete]
76
crud_rate_limits = CRUDRateLimit(RateLimit)

src/app/crud/crud_tier.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from fastcrud import FastCRUD
22

3-
from ..models.tier import Tier
4-
from ..schemas.tier import TierCreateInternal, TierDelete, TierUpdate, TierUpdateInternal
3+
from ..models.tier import Tier, TierCreateInternal, TierDelete, TierUpdate, TierUpdateInternal
54

65
CRUDTier = FastCRUD[Tier, TierCreateInternal, TierUpdate, TierUpdateInternal, TierDelete]
76
crud_tiers = CRUDTier(Tier)

src/app/crud/crud_users.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from fastcrud import FastCRUD
22

3-
from ..models.user import User
4-
from ..schemas.user import UserCreateInternal, UserDelete, UserUpdate, UserUpdateInternal
3+
from ..models.user import User, UserCreateInternal, UserDelete, UserUpdate, UserUpdateInternal
54

65
CRUDUser = FastCRUD[User, UserCreateInternal, UserUpdate, UserUpdateInternal, UserDelete]
76
crud_users = CRUDUser(User)

src/app/models/job.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from sqlmodel import SQLModel
2+
3+
4+
class Job(SQLModel):
5+
id: str

src/app/models/post.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,49 @@
1-
import uuid as uuid_pkg
2-
from datetime import UTC, datetime
1+
from datetime import datetime
2+
from typing import Optional
3+
from sqlmodel import SQLModel, Field, Relationship
4+
from uuid import uuid4
35

4-
from sqlalchemy import DateTime, ForeignKey, String
5-
from sqlalchemy.orm import Mapped, mapped_column
66

7-
from ..core.db.database import Base
7+
class PostBase(SQLModel):
8+
title: str = Field(..., min_length=2, max_length=30, schema_extra={"example": "This is my post"})
9+
text: str = Field(..., min_length=1, max_length=63206, schema_extra={"example": "This is the content of my post."})
810

911

10-
class Post(Base):
11-
__tablename__ = "post"
12+
class Post(PostBase, table=True):
13+
id: Optional[int] = Field(default=None, primary_key=True)
14+
created_by_user_id: int = Field(foreign_key="user.id")
15+
media_url: Optional[str] = Field(default=None, regex=r"^(https?|ftp)://[^\s/$.?#].[^\s]*$", schema_extra={"example": "https://www.postimageurl.com"})
16+
created_at: datetime = Field(default_factory=lambda: datetime.now(datetime.timezone.utc))
17+
updated_at: Optional[datetime] = None
18+
deleted_at: Optional[datetime] = None
19+
is_deleted: bool = Field(default=False)
1220

13-
id: Mapped[int] = mapped_column("id", autoincrement=True, nullable=False, unique=True, primary_key=True, init=False)
14-
created_by_user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), index=True)
15-
title: Mapped[str] = mapped_column(String(30))
16-
text: Mapped[str] = mapped_column(String(63206))
17-
uuid: Mapped[uuid_pkg.UUID] = mapped_column(default_factory=uuid_pkg.uuid4, primary_key=True, unique=True)
18-
media_url: Mapped[str | None] = mapped_column(String, default=None)
1921

20-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default_factory=lambda: datetime.now(UTC))
21-
updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
22-
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
23-
is_deleted: Mapped[bool] = mapped_column(default=False, index=True)
22+
class PostRead(PostBase):
23+
id: int
24+
created_by_user_id: int
25+
media_url: Optional[str]
26+
created_at: datetime
27+
28+
29+
class PostCreate(PostBase):
30+
media_url: Optional[str] = Field(default=None, regex=r"^(https?|ftp)://[^\s/$.?#].[^\s]*$", schema_extra={"example": "https://www.postimageurl.com"})
31+
32+
33+
class PostCreateInternal(PostCreate):
34+
created_by_user_id: int
35+
36+
37+
class PostUpdate(SQLModel):
38+
title: Optional[str] = Field(default=None, min_length=2, max_length=30)
39+
text: Optional[str] = Field(default=None, min_length=1, max_length=63206)
40+
media_url: Optional[str] = Field(default=None, regex=r"^(https?|ftp)://[^\s/$.?#].[^\s]*$")
41+
42+
43+
class PostUpdateInternal(PostUpdate):
44+
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
45+
46+
47+
class PostDelete(SQLModel):
48+
is_deleted: bool
49+
deleted_at: datetime

src/app/models/rate_limit.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,58 @@
1-
from datetime import UTC, datetime
1+
from datetime import datetime
2+
from typing import Optional
3+
from sqlmodel import SQLModel, Field
24

3-
from sqlalchemy import DateTime, ForeignKey, Integer, String
4-
from sqlalchemy.orm import Mapped, mapped_column
55

6-
from ..core.db.database import Base
6+
def sanitize_path(path: str) -> str:
7+
return path.strip("/").replace("/", "_")
78

89

9-
class RateLimit(Base):
10-
__tablename__ = "rate_limit"
10+
class RateLimitBase(SQLModel):
11+
path: str = Field(..., schema_extra={"example": "users"})
12+
limit: int = Field(..., schema_extra={"example": 5})
13+
period: int = Field(..., schema_extra={"example": 60})
1114

12-
id: Mapped[int] = mapped_column("id", autoincrement=True, nullable=False, unique=True, primary_key=True, init=False)
13-
tier_id: Mapped[int] = mapped_column(ForeignKey("tier.id"), index=True)
14-
name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
15-
path: Mapped[str] = mapped_column(String, nullable=False)
16-
limit: Mapped[int] = mapped_column(Integer, nullable=False)
17-
period: Mapped[int] = mapped_column(Integer, nullable=False)
15+
@classmethod
16+
def validate_path(cls, v: str) -> str:
17+
return sanitize_path(v)
1818

19-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default_factory=lambda: datetime.now(UTC))
20-
updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
19+
20+
class RateLimit(RateLimitBase, table=True):
21+
id: Optional[int] = Field(default=None, primary_key=True)
22+
tier_id: int = Field(foreign_key="tier.id")
23+
name: Optional[str] = Field(default=None, schema_extra={"example": "users:5:60"})
24+
created_at: datetime = Field(default_factory=lambda: datetime.now(datetime.timezone.utc))
25+
updated_at: Optional[datetime] = None
26+
27+
28+
class RateLimitRead(RateLimitBase):
29+
id: int
30+
tier_id: int
31+
name: str
32+
33+
34+
class RateLimitCreate(RateLimitBase):
35+
name: Optional[str] = Field(default=None, schema_extra={"example": "api_v1_users:5:60"})
36+
37+
38+
class RateLimitCreateInternal(RateLimitCreate):
39+
tier_id: int
40+
41+
42+
class RateLimitUpdate(SQLModel):
43+
path: Optional[str] = Field(default=None)
44+
limit: Optional[int] = None
45+
period: Optional[int] = None
46+
name: Optional[str] = None
47+
48+
@classmethod
49+
def validate_path(cls, v: Optional[str]) -> Optional[str]:
50+
return sanitize_path(v) if v is not None else None
51+
52+
53+
class RateLimitUpdateInternal(RateLimitUpdate):
54+
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
55+
56+
57+
class RateLimitDelete(SQLModel):
58+
pass

src/app/models/tier.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
1-
from datetime import UTC, datetime
1+
from datetime import datetime
2+
from typing import Optional
3+
from sqlmodel import SQLModel, Field
24

3-
from sqlalchemy import DateTime, String
4-
from sqlalchemy.orm import Mapped, mapped_column
55

6-
from ..core.db.database import Base
6+
class TierBase(SQLModel):
7+
name: str = Field(..., schema_extra={"example": "free"})
78

89

9-
class Tier(Base):
10-
__tablename__ = "tier"
10+
class Tier(TierBase, table=True):
11+
id: Optional[int] = Field(default=None, primary_key=True)
12+
created_at: datetime = Field(default_factory=lambda: datetime.now(datetime.timezone.utc))
13+
updated_at: Optional[datetime] = None
1114

12-
id: Mapped[int] = mapped_column("id", autoincrement=True, nullable=False, unique=True, primary_key=True, init=False)
13-
name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
1415

15-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default_factory=lambda: datetime.now(UTC))
16-
updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
16+
class TierRead(TierBase):
17+
id: int
18+
created_at: datetime
19+
20+
21+
class TierCreate(TierBase):
22+
pass
23+
24+
25+
class TierCreateInternal(TierCreate):
26+
pass
27+
28+
29+
class TierUpdate(SQLModel):
30+
name: Optional[str] = None
31+
32+
33+
class TierUpdateInternal(TierUpdate):
34+
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
35+
36+
37+
class TierDelete(SQLModel):
38+
pass

0 commit comments

Comments
 (0)