diff options
| author | l3wdfut4pwr <l3wdfut4pwr@gmail.com> | 2026-04-04 00:03:04 +0300 |
|---|---|---|
| committer | l3wdfut4pwr <l3wdfut4pwr@gmail.com> | 2026-04-04 00:03:04 +0300 |
| commit | 63e87a3ed84ee9e9e4a4ff329a40d2b7ce5e5d0d (patch) | |
| tree | c24731c200df3b5854a5abc14f7a5481a33d838a /app | |
| parent | d835d79eb24c730ec8148415113e846a01cefd19 (diff) | |
add profile
Diffstat (limited to 'app')
| -rw-r--r-- | app/models/__init__.py | 5 | ||||
| -rw-r--r-- | app/models/collections.py | 40 | ||||
| -rw-r--r-- | app/models/image.py | 9 | ||||
| -rw-r--r-- | app/models/integrations.py | 13 | ||||
| -rw-r--r-- | app/models/profile.py | 38 | ||||
| -rw-r--r-- | app/models/user.py | 33 | ||||
| -rw-r--r-- | app/routes/__init__.py | 3 | ||||
| -rw-r--r-- | app/routes/register.py | 28 | ||||
| -rw-r--r-- | app/routes/user.py | 21 | ||||
| -rw-r--r-- | app/schemas/integrations.py | 15 | ||||
| -rw-r--r-- | app/schemas/profile.py | 21 | ||||
| -rw-r--r-- | app/schemas/user.py | 7 | ||||
| -rw-r--r-- | app/utils/__init__.py | 17 | ||||
| -rw-r--r-- | app/utils/db.py | 20 |
14 files changed, 206 insertions, 64 deletions
diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29..603a57f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +# from .collections import Collection +from .image import Image +from .integrations import UserIntegration +from .profile import Profile +from .user import User diff --git a/app/models/collections.py b/app/models/collections.py index 7dc5bb7..e69de29 100644 --- a/app/models/collections.py +++ b/app/models/collections.py @@ -1,40 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from typing import TYPE_CHECKING - -from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Table -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship - -Base = DeclarativeBase() - -if TYPE_CHECKING: - from .image import Image - -collection_images = Table( - "collection_images", - Base.metadata, - mapped_column( - "collection_id", Integer, ForeignKey("collections.id"), primary_key=True - ), - mapped_column("image_id", Integer, ForeignKey("images.id"), primary_key=True), -) - - -class Collection(Base): - __tablename__ = "collections" - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) - name: Mapped[str] = mapped_column(String(100), nullable=False) - description: Mapped[str | None] = mapped_column(String(500), nullable=True) - is_private: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) - created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), default=datetime.utcnow - ) - - images: Mapped[list["Image"]] = relationship( - "Image", - secondary=collection_images, - back_populates="collections", - ) diff --git a/app/models/image.py b/app/models/image.py index e139123..b75b197 100644 --- a/app/models/image.py +++ b/app/models/image.py @@ -12,11 +12,12 @@ from sqlalchemy import ( Table, func, ) -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship -from .collections import Collection +from app.utils.db import Base + +# from .collections import Collection -Base = DeclarativeBase() image_tags = Table( "image_tags", @@ -63,8 +64,10 @@ class Image(Base): ) +""" collections: Mapped[list["Collection"]] = relationship( "Collection", secondary="collection_images", back_populates="images", ) +""" diff --git a/app/models/integrations.py b/app/models/integrations.py index 4a1ddb9..f3eaca4 100644 --- a/app/models/integrations.py +++ b/app/models/integrations.py @@ -1,7 +1,12 @@ +from typing import TYPE_CHECKING + from sqlalchemy import ForeignKey, Integer, String -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.utils.db import Base -Base = DeclarativeBase() +if TYPE_CHECKING: + from .user import User class UserIntegration(Base): @@ -17,3 +22,7 @@ class UserIntegration(Base): x: Mapped[str | None] = mapped_column(String(255), nullable=True) # Twitter/X behance: Mapped[str | None] = mapped_column(String(255), nullable=True) instagram: Mapped[str | None] = mapped_column(String(255), nullable=True) + + user: Mapped["User"] = relationship( + "User", back_populates="integrations", uselist=False + ) diff --git a/app/models/profile.py b/app/models/profile.py index b19d796..93ce6fd 100644 --- a/app/models/profile.py +++ b/app/models/profile.py @@ -1,14 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fastapi import Depends from sqlalchemy import ForeignKey, Integer, String -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.utils.db import Base, get_async_session -Base = DeclarativeBase() +if TYPE_CHECKING: + from .user import ( + User, + ) class Profile(Base): __tablename__ = "profiles" id: Mapped[int] = mapped_column(Integer, primary_key=True) - user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), unique=True, nullable=False + ) avatar_file: Mapped[str | None] = mapped_column(String(255), nullable=True) banner_file: Mapped[str | None] = mapped_column(String(255), nullable=True) @@ -19,3 +33,21 @@ class Profile(Base): subscriptions_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) followers_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) following_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + + user: Mapped["User"] = relationship( + "User", back_populates="profile", uselist=False, lazy="selectin" + ) + + @classmethod + async def get_by_user_id( + cls, user_id: int, session: AsyncSession = Depends(get_async_session) + ): + result = await session.execute(select(cls).where(cls.user_id == user_id)) + return result.scalars().first() + + @classmethod + async def get_by_id( + cls, profile_id: int, session: AsyncSession = Depends(get_async_session) + ): + result = await session.execute(select(cls).where(cls.id == profile_id)) + return result.scalars().first() diff --git a/app/models/user.py b/app/models/user.py index 8dbe419..442173c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,14 +1,18 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from fastapi import Depends from sqlalchemy import Boolean, Integer, String from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column - -from app.utils.db import get_async_session +from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload +from app.utils.db import Base, get_async_session -class Base(DeclarativeBase): - pass +if TYPE_CHECKING: + from .integrations import UserIntegration + from .profile import Profile class User(Base): @@ -21,13 +25,19 @@ class User(Base): google_id: Mapped[str | None] = mapped_column( String(255), unique=True, nullable=True ) - avatar_file: Mapped[str | None] = mapped_column(String(255), nullable=True) - banner_file: Mapped[str | None] = mapped_column(String(255), nullable=True) + description: Mapped[str | None] = mapped_column(String(250), nullable=True) premium: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_banned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_moderator: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) token_version: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + profile: Mapped["Profile"] = relationship( + "Profile", back_populates="user", uselist=False, lazy="selectin" + ) + integrations: Mapped["UserIntegration"] = relationship( + "UserIntegration", back_populates="user", uselist=False, lazy="selectin" + ) + @classmethod async def get_user_by_email( cls, email: str, session: AsyncSession = Depends(get_async_session) @@ -39,7 +49,14 @@ class User(Base): async def get_user_by_username( cls, username: str, session: AsyncSession = Depends(get_async_session) ): - result = await session.execute(select(cls).where(cls.username == username)) + result = await session.execute( + select(cls) + .options( + selectinload(cls.profile), + selectinload(cls.integrations), + ) + .where(cls.username == username) + ) return result.scalars().first() @classmethod diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 383ef66..a57869a 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -3,10 +3,11 @@ from fastapi import APIRouter from .auth import router as auth_router from .me import router as me_router from .register import router as register_router +from .user import router as user_router router = APIRouter() router.include_router(register_router, prefix="/auth") router.include_router(auth_router, prefix="/auth") - +router.include_router(user_router) router.include_router(me_router) diff --git a/app/routes/register.py b/app/routes/register.py index ffcd336..779cf73 100644 --- a/app/routes/register.py +++ b/app/routes/register.py @@ -4,8 +4,10 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Response from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from sqlalchemy.orm import selectinload from app.auth.jwt import create_access_token, create_refresh_token +from app.models.profile import Profile from app.models.user import User from app.schemas.user import UserCreate, UserRead from app.utils.db import get_async_session @@ -69,8 +71,32 @@ async def register_user( new_user = User(username=user.username, email=email, password=hashed_password) session.add(new_user) + await session.flush() + + new_profile = Profile(user_id=new_user.id) + session.add(new_profile) + + from app.models.integrations import UserIntegration + + new_integrations = UserIntegration(user_id=new_user.id) + session.add(new_integrations) + await session.commit() - await session.refresh(new_user) + + result = await session.execute( + select(User) + .options( + selectinload(User.profile), + selectinload(User.integrations), + ) + .where(User.id == new_user.id) + ) + new_user = result.scalars().first() + + result = await session.execute( + select(User).options(selectinload(User.profile)).where(User.id == new_user.id) + ) + new_user = result.scalars().first() logger.success( "User successfully registered | id={} username={} email={}", diff --git a/app/routes/user.py b/app/routes/user.py new file mode 100644 index 0000000..0860cc1 --- /dev/null +++ b/app/routes/user.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user import User +from app.schemas.user import UserRead +from app.utils.db import get_async_session + +router = APIRouter(prefix="/users", tags=["users"]) + + +@router.get("/{username}", response_model=UserRead) +async def get_user( + username: str, + session: AsyncSession = Depends(get_async_session), +): + user = await User.get_user_by_username(username, session=session) + + if not user: + raise HTTPException(status_code=404, detail="User not found") + + return user diff --git a/app/schemas/integrations.py b/app/schemas/integrations.py new file mode 100644 index 0000000..6383d5b --- /dev/null +++ b/app/schemas/integrations.py @@ -0,0 +1,15 @@ +from typing import Optional + +from pydantic import BaseModel + + +class UserIntegrationRead(BaseModel): + facebook: Optional[str] = None + pinterest: Optional[str] = None + discord: Optional[str] = None + artstation: Optional[str] = None + x: Optional[str] = None + behance: Optional[str] = None + instagram: Optional[str] = None + + model_config = {"from_attributes": True} diff --git a/app/schemas/profile.py b/app/schemas/profile.py new file mode 100644 index 0000000..ab54425 --- /dev/null +++ b/app/schemas/profile.py @@ -0,0 +1,21 @@ +from typing import Optional + +from pydantic import BaseModel + + +class ProfileRead(BaseModel): + id: int + user_id: int + avatar_file: Optional[str] = None + banner_file: Optional[str] = None + description: Optional[str] = None + + publications_count: int = 0 + collections_count: int = 0 + subscriptions_count: int = 0 + followers_count: int = 0 + following_count: int = 0 + + model_config = { + "from_attributes": True, + } diff --git a/app/schemas/user.py b/app/schemas/user.py index 6644216..83a3245 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -2,6 +2,10 @@ from typing import Optional from pydantic import BaseModel, EmailStr, Field +from app.schemas.profile import ProfileRead + +from .integrations import UserIntegrationRead + class UserCreate(BaseModel): username: str = Field(..., max_length=32) @@ -23,7 +27,8 @@ class UserRead(BaseModel): premium: bool is_banned: bool is_moderator: bool - + profile: Optional[ProfileRead] = None + integrations: Optional[UserIntegrationRead] = None model_config = { "from_attributes": True, } diff --git a/app/utils/__init__.py b/app/utils/__init__.py index e69de29..a05c4d1 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -0,0 +1,17 @@ +from .cors import setup_cors +from .create_tables import init_db +from .db import Base, async_session, engine, get_async_session +from .hash_cfg import hash_password, verify_password +from .logger_cfg import logger + +__all__ = [ + "engine", + "async_session", + "get_async_session", + "Base", + "setup_cors", + "init_db", + "hash_password", + "verify_password", + "logger", +] diff --git a/app/utils/db.py b/app/utils/db.py index b823904..4daca74 100644 --- a/app/utils/db.py +++ b/app/utils/db.py @@ -1,3 +1,4 @@ +import asyncio import os from dotenv import load_dotenv @@ -5,20 +6,29 @@ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import DeclarativeBase, sessionmaker load_dotenv() - DATABASE_URL = os.getenv("DATABASE_URL") if not DATABASE_URL: - raise ValueError("DATABASE_URL not found .env") + raise ValueError("DATABASE_URL not found in .env") engine = create_async_engine(DATABASE_URL, echo=True, future=True) - async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) +class Base(DeclarativeBase): + pass + + async def get_async_session() -> AsyncSession: async with async_session() as session: yield session -class Base(DeclarativeBase): - pass +async def init_db(): + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +if __name__ == "__main__": + asyncio.run(init_db()) + print("All tables created!") |
