summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/models/__init__.py5
-rw-r--r--app/models/collections.py40
-rw-r--r--app/models/image.py9
-rw-r--r--app/models/integrations.py13
-rw-r--r--app/models/profile.py38
-rw-r--r--app/models/user.py33
-rw-r--r--app/routes/__init__.py3
-rw-r--r--app/routes/register.py28
-rw-r--r--app/routes/user.py21
-rw-r--r--app/schemas/integrations.py15
-rw-r--r--app/schemas/profile.py21
-rw-r--r--app/schemas/user.py7
-rw-r--r--app/utils/__init__.py17
-rw-r--r--app/utils/db.py20
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!")