diff --git a/app/home/models.py b/app/home/models.py index 026523d..f3c7941 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -149,7 +149,7 @@ class Project(Base): id: 고유 식별자 (자동 증가) store_name: 고객명 (필수) region: 지역명 (필수, 예: 서울, 부산, 대구 등) - task_id: 작업 고유 식별자 (UUID 형식, 36자) + task_id: 작업 고유 식별자 (KSUID 형식, 27자) detail_region_info: 상세 지역 정보 (선택, JSON 또는 텍스트 형식) created_at: 생성 일시 (자동 설정) @@ -184,21 +184,20 @@ class Project(Base): store_name: Mapped[str] = mapped_column( String(255), nullable=False, - index=True, comment="가게명", ) region: Mapped[str] = mapped_column( String(100), nullable=False, - index=True, comment="지역명 (예: 군산)", ) task_id: Mapped[str] = mapped_column( - String(36), + String(27), nullable=False, - comment="프로젝트 작업 고유 식별자 (UUID)", + unique=True, + comment="프로젝트 작업 고유 식별자 (KSUID)", ) detail_region_info: Mapped[Optional[str]] = mapped_column( @@ -281,7 +280,7 @@ class Image(Base): Attributes: id: 고유 식별자 (자동 증가) - task_id: 이미지 업로드 작업 고유 식별자 (UUID) + task_id: 이미지 업로드 작업 고유 식별자 (KSUID) img_name: 이미지명 img_url: 이미지 URL (S3, CDN 등의 경로) created_at: 생성 일시 (자동 설정) @@ -289,6 +288,7 @@ class Image(Base): __tablename__ = "image" __table_args__ = ( + Index("idx_image_task_id", "task_id"), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", @@ -305,9 +305,10 @@ class Image(Base): ) task_id: Mapped[str] = mapped_column( - String(36), + String(27), nullable=False, - comment="이미지 업로드 작업 고유 식별자 (UUID)", + unique=True, + comment="이미지 업로드 작업 고유 식별자 (KSUID)", ) img_name: Mapped[str] = mapped_column( diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index 79f73da..3aca6f5 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -91,7 +91,7 @@ class GenerateUrlsRequest(GenerateRequestInfo): class GenerateUploadResponse(BaseModel): """파일 업로드 기반 생성 응답 스키마""" - task_id: str = Field(..., description="작업 고유 식별자 (UUID7)") + task_id: str = Field(..., description="작업 고유 식별자 (KSUID)") status: Literal["processing", "completed", "failed"] = Field( ..., description="작업 상태" ) @@ -102,7 +102,7 @@ class GenerateUploadResponse(BaseModel): class GenerateResponse(BaseModel): """생성 응답 스키마""" - task_id: str = Field(..., description="작업 고유 식별자 (UUID7)") + task_id: str = Field(..., description="작업 고유 식별자 (KSUID)") status: Literal["processing", "completed", "failed"] = Field( ..., description="작업 상태" ) @@ -291,7 +291,7 @@ class ImageUploadResponse(BaseModel): } ) - task_id: str = Field(..., description="작업 고유 식별자 (새로 생성된 UUID7)") + task_id: str = Field(..., description="작업 고유 식별자 (새로 생성된 KSUID)") total_count: int = Field(..., description="총 업로드된 이미지 개수") url_count: int = Field(..., description="URL로 등록된 이미지 개수") file_count: int = Field(..., description="파일로 업로드된 이미지 개수") diff --git a/app/lyric/models.py b/app/lyric/models.py index 8a5c0a9..c0cb64b 100644 --- a/app/lyric/models.py +++ b/app/lyric/models.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import TYPE_CHECKING, List -from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, func from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -23,7 +23,7 @@ class Lyric(Base): Attributes: id: 고유 식별자 (자동 증가) project_id: 연결된 Project의 id (외래키) - task_id: 가사 생성 작업의 고유 식별자 (UUID 형식) + task_id: 가사 생성 작업의 고유 식별자 (KSUID 형식) status: 처리 상태 (pending, processing, completed, failed 등) lyric_prompt: 가사 생성에 사용된 프롬프트 lyric_result: 생성된 가사 결과 (LONGTEXT로 긴 가사 지원) @@ -37,6 +37,8 @@ class Lyric(Base): __tablename__ = "lyric" __table_args__ = ( + Index("idx_lyric_task_id", "task_id"), + Index("idx_lyric_project_id", "project_id"), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", @@ -56,14 +58,14 @@ class Lyric(Base): Integer, ForeignKey("project.id", ondelete="CASCADE"), nullable=False, - index=True, comment="연결된 Project의 id", ) task_id: Mapped[str] = mapped_column( - String(36), + String(27), nullable=False, - comment="가사 생성 작업 고유 식별자 (UUID)", + unique=True, + comment="가사 생성 작업 고유 식별자 (KSUID)", ) status: Mapped[str] = mapped_column( diff --git a/app/song/models.py b/app/song/models.py index 8f6d7d6..341a796 100644 --- a/app/song/models.py +++ b/app/song/models.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import TYPE_CHECKING, List, Optional -from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func +from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.session import Base @@ -23,7 +23,7 @@ class Song(Base): id: 고유 식별자 (자동 증가) project_id: 연결된 Project의 id (외래키) lyric_id: 연결된 Lyric의 id (외래키) - task_id: 노래 생성 작업의 고유 식별자 (UUID 형식) + task_id: 노래 생성 작업의 고유 식별자 (KSUID 형식) suno_task_id: Suno API 작업 고유 식별자 (선택) status: 처리 상태 (processing, uploading, completed, failed) song_prompt: 노래 생성에 사용된 프롬프트 @@ -39,6 +39,9 @@ class Song(Base): __tablename__ = "song" __table_args__ = ( + Index("idx_song_task_id", "task_id"), + Index("idx_song_project_id", "project_id"), + Index("idx_song_lyric_id", "lyric_id"), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", @@ -58,7 +61,6 @@ class Song(Base): Integer, ForeignKey("project.id", ondelete="CASCADE"), nullable=False, - index=True, comment="연결된 Project의 id", ) @@ -66,14 +68,14 @@ class Song(Base): Integer, ForeignKey("lyric.id", ondelete="CASCADE"), nullable=False, - index=True, comment="연결된 Lyric의 id", ) task_id: Mapped[str] = mapped_column( - String(36), + String(27), nullable=False, - comment="노래 생성 작업 고유 식별자 (UUID)", + unique=True, + comment="노래 생성 작업 고유 식별자 (KSUID)", ) suno_task_id: Mapped[Optional[str]] = mapped_column( @@ -177,6 +179,7 @@ class SongTimestamp(Base): __tablename__ = "song_timestamp" __table_args__ = ( + Index("idx_song_timestamp_suno_audio_id", "suno_audio_id"), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", @@ -195,7 +198,6 @@ class SongTimestamp(Base): suno_audio_id: Mapped[str] = mapped_column( String(64), nullable=False, - index=True, comment="가사의 원본 오디오 ID", ) diff --git a/app/user/dependencies/auth.py b/app/user/dependencies/auth.py index 441762b..cd8701a 100644 --- a/app/user/dependencies/auth.py +++ b/app/user/dependencies/auth.py @@ -58,14 +58,14 @@ async def get_current_user( if payload.get("type") != "access": raise InvalidTokenError("액세스 토큰이 아닙니다.") - user_id = payload.get("sub") - if user_id is None: + user_ksuid = payload.get("sub") + if user_ksuid is None: raise InvalidTokenError() # 사용자 조회 result = await session.execute( select(User).where( - User.id == int(user_id), + User.user_ksuid == user_ksuid, User.is_deleted == False, # noqa: E712 ) ) @@ -106,13 +106,13 @@ async def get_current_user_optional( if payload.get("type") != "access": return None - user_id = payload.get("sub") - if user_id is None: + user_ksuid = payload.get("sub") + if user_ksuid is None: return None result = await session.execute( select(User).where( - User.id == int(user_id), + User.user_ksuid == user_ksuid, User.is_deleted == False, # noqa: E712 ) ) diff --git a/app/user/models.py b/app/user/models.py index 72635fd..ae3d603 100644 --- a/app/user/models.py +++ b/app/user/models.py @@ -26,6 +26,7 @@ class User(Base): Attributes: id: 고유 식별자 (자동 증가) kakao_id: 카카오 고유 ID (필수, 유니크) + user_ksuid: 사용자 식별을 위한 KSUID (필수, 유니크) email: 이메일 주소 (선택, 카카오에서 제공 시) nickname: 카카오 닉네임 (선택) profile_image_url: 카카오 프로필 이미지 URL (선택) @@ -68,7 +69,8 @@ class User(Base): __tablename__ = "user" __table_args__ = ( - Index("idx_user_kakao_id", "kakao_id", unique=True), + Index("idx_user_kakao_id", "kakao_id"), + Index("idx_user_ksuid", "user_ksuid"), Index("idx_user_email", "email"), Index("idx_user_phone", "phone"), Index("idx_user_is_active", "is_active"), @@ -103,6 +105,13 @@ class User(Base): comment="카카오 고유 ID (회원번호)", ) + user_ksuid: Mapped[str] = mapped_column( + String(27), + nullable=False, + unique=True, + comment="사용자 식별을 위한 KSUID (K-Sortable Unique Identifier)", + ) + # ========================================================================== # 카카오에서 제공하는 사용자 정보 (선택적) # ========================================================================== @@ -282,6 +291,7 @@ class RefreshToken(Base): Attributes: id: 고유 식별자 (자동 증가) user_id: 사용자 외래키 (User.id 참조) + user_ksuid: 사용자 KSUID (User.user_ksuid 참조) token_hash: 리프레시 토큰의 SHA-256 해시값 (원본 저장 X) expires_at: 토큰 만료 일시 is_revoked: 토큰 폐기 여부 (로그아웃 시 True) @@ -299,6 +309,7 @@ class RefreshToken(Base): __tablename__ = "refresh_token" __table_args__ = ( Index("idx_refresh_token_user_id", "user_id"), + Index("idx_refresh_token_user_ksuid", "user_ksuid"), Index("idx_refresh_token_token_hash", "token_hash", unique=True), Index("idx_refresh_token_expires_at", "expires_at"), Index("idx_refresh_token_is_revoked", "is_revoked"), @@ -324,6 +335,12 @@ class RefreshToken(Base): comment="사용자 외래키 (User.id 참조)", ) + user_ksuid: Mapped[str] = mapped_column( + String(27), + nullable=False, + comment="사용자 KSUID (User.user_ksuid 참조)", + ) + token_hash: Mapped[str] = mapped_column( String(64), nullable=False, diff --git a/app/user/schemas/user_schema.py b/app/user/schemas/user_schema.py index dee7fe6..918f4c5 100644 --- a/app/user/schemas/user_schema.py +++ b/app/user/schemas/user_schema.py @@ -156,13 +156,13 @@ class UserBriefResponse(BaseModel): class LoginResponse(BaseModel): - """로그인 응답 (토큰 + 사용자 정보)""" + """로그인 응답 (토큰 정보)""" access_token: str = Field(..., description="액세스 토큰") refresh_token: str = Field(..., description="리프레시 토큰") token_type: str = Field(default="Bearer", description="토큰 타입") expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)") - user: UserBriefResponse = Field(..., description="사용자 정보") + is_new_user: bool = Field(..., description="신규 가입 여부") redirect_url: str = Field(..., description="로그인 후 리다이렉트할 프론트엔드 URL") model_config = { @@ -172,13 +172,7 @@ class LoginResponse(BaseModel): "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3MDU4MzM2MDB9.yyy", "token_type": "Bearer", "expires_in": 3600, - "user": { - "id": 1, - "nickname": "홍길동", - "email": "user@kakao.com", - "profile_image_url": "https://k.kakaocdn.net/dn/.../profile.jpg", - "is_new_user": False - }, + "is_new_user": False, "redirect_url": "http://localhost:3000" } } diff --git a/app/user/services/auth.py b/app/user/services/auth.py index fe83bfb..f5dcb36 100644 --- a/app/user/services/auth.py +++ b/app/user/services/auth.py @@ -24,11 +24,11 @@ from app.user.exceptions import ( UserNotFoundError, ) from app.user.models import RefreshToken, User +from app.utils.common import generate_ksuid from app.user.schemas.user_schema import ( AccessTokenResponse, KakaoUserInfo, LoginResponse, - UserBriefResponse, ) from app.user.services.jwt import ( create_access_token, @@ -96,27 +96,28 @@ class AuthService: # 5. JWT 토큰 생성 logger.info("[AUTH] 5단계: JWT 토큰 생성 시작") - access_token = create_access_token(user.id) - refresh_token = create_refresh_token(user.id) - logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_id: {user.id}") + access_token = create_access_token(user.user_ksuid) + refresh_token = create_refresh_token(user.user_ksuid) + logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_ksuid: {user.user_ksuid}") # 6. 리프레시 토큰 DB 저장 logger.info("[AUTH] 6단계: 리프레시 토큰 저장 시작") await self._save_refresh_token( user_id=user.id, + user_ksuid=user.user_ksuid, token=refresh_token, session=session, user_agent=user_agent, ip_address=ip_address, ) - logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}") + logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}, user_ksuid: {user.user_ksuid}") # 7. 마지막 로그인 시간 업데이트 user.last_login_at = datetime.now(timezone.utc) await session.commit() redirect_url = f"{prj_settings.PROJECT_DOMAIN}" - logger.info(f"[AUTH] 카카오 로그인 완료 - user_id: {user.id}, redirect_url: {redirect_url}") + logger.info(f"[AUTH] 카카오 로그인 완료 - user_id: {user.id}, user_ksuid: {user.user_ksuid}, redirect_url: {redirect_url}") logger.debug(f"[AUTH] 응답 토큰 정보 - access_token: {access_token[:30]}..., refresh_token: {refresh_token[:30]}...") return LoginResponse( @@ -124,13 +125,7 @@ class AuthService: refresh_token=refresh_token, token_type="Bearer", expires_in=get_access_token_expire_seconds(), - user=UserBriefResponse( - id=user.id, - nickname=user.nickname, - email=user.email, - profile_image_url=user.profile_image_url, - is_new_user=is_new_user, - ), + is_new_user=is_new_user, redirect_url=redirect_url, ) @@ -177,8 +172,8 @@ class AuthService: raise TokenExpiredError() # 4. 사용자 확인 - user_id = int(payload.get("sub")) - user = await self._get_user_by_id(user_id, session) + user_ksuid = payload.get("sub") + user = await self._get_user_by_ksuid(user_ksuid, session) if user is None: raise UserNotFoundError() @@ -187,7 +182,7 @@ class AuthService: raise UserInactiveError() # 5. 새 액세스 토큰 발급 - new_access_token = create_access_token(user.id) + new_access_token = create_access_token(user.user_ksuid) return AccessTokenResponse( access_token=new_access_token, @@ -269,8 +264,10 @@ class AuthService: # 신규 사용자 생성 logger.info(f"[AUTH] 신규 사용자 생성 시작 - kakao_id: {kakao_id}") + ksuid = await generate_ksuid(session=session, table_name=User) new_user = User( kakao_id=kakao_id, + user_ksuid=ksuid, email=kakao_account.email if kakao_account else None, nickname=profile.nickname if profile else None, profile_image_url=profile.profile_image_url if profile else None, @@ -319,6 +316,7 @@ class AuthService: async def _save_refresh_token( self, user_id: int, + user_ksuid: str, token: str, session: AsyncSession, user_agent: Optional[str] = None, @@ -329,6 +327,7 @@ class AuthService: Args: user_id: 사용자 ID + user_ksuid: 사용자 KSUID token: 리프레시 토큰 session: DB 세션 user_agent: User-Agent @@ -342,6 +341,7 @@ class AuthService: refresh_token = RefreshToken( user_id=user_id, + user_ksuid=user_ksuid, token_hash=token_hash, expires_at=expires_at, user_agent=user_agent, @@ -391,6 +391,26 @@ class AuthService: ) return result.scalar_one_or_none() + async def _get_user_by_ksuid( + self, + user_ksuid: str, + session: AsyncSession, + ) -> Optional[User]: + """ + KSUID로 사용자 조회 + + Args: + user_ksuid: 사용자 KSUID + session: DB 세션 + + Returns: + User 객체 또는 None + """ + result = await session.execute( + select(User).where(User.user_ksuid == user_ksuid, User.is_deleted == False) # noqa: E712 + ) + return result.scalar_one_or_none() + async def _revoke_refresh_token_by_hash( self, token_hash: str, diff --git a/app/user/services/jwt.py b/app/user/services/jwt.py index 91c2eee..8edb693 100644 --- a/app/user/services/jwt.py +++ b/app/user/services/jwt.py @@ -13,12 +13,12 @@ from jose import JWTError, jwt from config import jwt_settings -def create_access_token(user_id: int) -> str: +def create_access_token(user_ksuid: str) -> str: """ JWT 액세스 토큰 생성 Args: - user_id: 사용자 ID + user_ksuid: 사용자 KSUID Returns: JWT 액세스 토큰 문자열 @@ -27,7 +27,7 @@ def create_access_token(user_id: int) -> str: minutes=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES ) to_encode = { - "sub": str(user_id), + "sub": user_ksuid, "exp": expire, "type": "access", } @@ -38,12 +38,12 @@ def create_access_token(user_id: int) -> str: ) -def create_refresh_token(user_id: int) -> str: +def create_refresh_token(user_ksuid: str) -> str: """ JWT 리프레시 토큰 생성 Args: - user_id: 사용자 ID + user_ksuid: 사용자 KSUID Returns: JWT 리프레시 토큰 문자열 @@ -52,7 +52,7 @@ def create_refresh_token(user_id: int) -> str: days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS ) to_encode = { - "sub": str(user_id), + "sub": user_ksuid, "exp": expire, "type": "refresh", } diff --git a/app/utils/common.py b/app/utils/common.py index 72ad241..baef8b9 100644 --- a/app/utils/common.py +++ b/app/utils/common.py @@ -4,11 +4,14 @@ Common Utility Functions 공통으로 사용되는 유틸리티 함수들을 정의합니다. 사용 예시: - from app.utils.common import generate_task_id + from app.utils.common import generate_task_id, generate_ksuid # task_id 생성 task_id = await generate_task_id(session=session, table_name=Project) + # ksuid 생성 + ksuid = await generate_ksuid(session=session, table_name=User) + Note: 페이지네이션 기능은 app.utils.pagination 모듈을 사용하세요: from app.utils.pagination import PaginatedResponse, get_paginated @@ -18,7 +21,7 @@ from typing import Any, Optional, Type from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_extensions import uuid7 +from svix_ksuid import Ksuid async def generate_task_id( @@ -32,16 +35,16 @@ async def generate_task_id( table_name: task_id 컬럼이 있는 SQLAlchemy 테이블 클래스 (optional) Returns: - str: 생성된 uuid7 문자열 + str: 생성된 ksuid 문자열 Usage: - # 단순 uuid7 생성 + # 단순 ksuid 생성 task_id = await generate_task_id() # 테이블에서 중복 검사 후 생성 task_id = await generate_task_id(session=session, table_name=Project) """ - task_id = str(uuid7()) + task_id = str(Ksuid()) if session is None or table_name is None: return task_id @@ -55,4 +58,41 @@ async def generate_task_id( if existing is None: return task_id - task_id = str(uuid7()) + task_id = str(Ksuid()) + + +async def generate_ksuid( + session: Optional[AsyncSession] = None, + table_name: Optional[Type[Any]] = None, +) -> str: + """고유한 ksuid를 생성합니다. + + Args: + session: SQLAlchemy AsyncSession (optional) + table_name: user_ksuid 컬럼이 있는 SQLAlchemy 테이블 클래스 (optional) + + Returns: + str: 생성된 ksuid 문자열 + + Usage: + # 단순 ksuid 생성 + ksuid = await generate_ksuid() + + # 테이블에서 중복 검사 후 생성 + ksuid = await generate_ksuid(session=session, table_name=User) + """ + ksuid = str(Ksuid()) + + if session is None or table_name is None: + return ksuid + + while True: + result = await session.execute( + select(table_name).where(table_name.user_ksuid == ksuid) + ) + existing = result.scalar_one_or_none() + + if existing is None: + return ksuid + + ksuid = str(Ksuid()) diff --git a/app/video/models.py b/app/video/models.py index 9bcce0b..b047027 100644 --- a/app/video/models.py +++ b/app/video/models.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Optional -from sqlalchemy import DateTime, ForeignKey, Integer, String, func +from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.session import Base @@ -24,7 +24,7 @@ class Video(Base): project_id: 연결된 Project의 id (외래키) lyric_id: 연결된 Lyric의 id (외래키) song_id: 연결된 Song의 id (외래키) - task_id: 영상 생성 작업의 고유 식별자 (UUID 형식) + task_id: 영상 생성 작업의 고유 식별자 (KSUID 형식) status: 처리 상태 (pending, processing, completed, failed 등) result_movie_url: 생성된 영상 URL (S3, CDN 경로) created_at: 생성 일시 (자동 설정) @@ -37,6 +37,10 @@ class Video(Base): __tablename__ = "video" __table_args__ = ( + Index("idx_video_task_id", "task_id"), + Index("idx_video_project_id", "project_id"), + Index("idx_video_lyric_id", "lyric_id"), + Index("idx_video_song_id", "song_id"), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", @@ -56,7 +60,6 @@ class Video(Base): Integer, ForeignKey("project.id", ondelete="CASCADE"), nullable=False, - index=True, comment="연결된 Project의 id", ) @@ -64,7 +67,6 @@ class Video(Base): Integer, ForeignKey("lyric.id", ondelete="CASCADE"), nullable=False, - index=True, comment="연결된 Lyric의 id", ) @@ -72,15 +74,14 @@ class Video(Base): Integer, ForeignKey("song.id", ondelete="CASCADE"), nullable=False, - index=True, comment="연결된 Song의 id", ) task_id: Mapped[str] = mapped_column( - String(36), + String(27), nullable=False, - index=True, - comment="영상 생성 작업 고유 식별자 (UUID)", + unique=True, + comment="영상 생성 작업 고유 식별자 (KSUID)", ) creatomate_render_id: Mapped[Optional[str]] = mapped_column( diff --git a/pyproject.toml b/pyproject.toml index d5e984d..56748e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "scalar-fastapi>=1.6.1", "sqladmin[full]>=0.22.0", "sqlalchemy[asyncio]>=2.0.45", + "svix-ksuid>=0.6.2", "uuid7>=0.1.0", ] diff --git a/uv.lock b/uv.lock index 9c9dcaf..6ef9c17 100644 --- a/uv.lock +++ b/uv.lock @@ -725,6 +725,7 @@ dependencies = [ { name = "scalar-fastapi" }, { name = "sqladmin", extra = ["full"] }, { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "svix-ksuid" }, { name = "uuid7" }, ] @@ -753,6 +754,7 @@ requires-dist = [ { name = "scalar-fastapi", specifier = ">=1.6.1" }, { name = "sqladmin", extras = ["full"], specifier = ">=0.22.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.45" }, + { name = "svix-ksuid", specifier = ">=0.6.2" }, { name = "uuid7", specifier = ">=0.1.0" }, ] @@ -1019,6 +1021,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "python-baseconv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/d0/9297d7d8dd74767b4d5560d834b30b2fff17d39987c23ed8656f476e0d9b/python-baseconv-1.2.2.tar.gz", hash = "sha256:0539f8bd0464013b05ad62e0a1673f0ac9086c76b43ebf9f833053527cd9931b", size = 4929, upload-time = "2019-04-04T19:28:57.17Z" } + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1314,6 +1322,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] +[[package]] +name = "svix-ksuid" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-baseconv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/7a/0c98b77ca01d64f13143607b88273a13110d659780b93cb1333abebf8039/svix-ksuid-0.6.2.tar.gz", hash = "sha256:beb95bd6284bdbd526834e233846653d2bd26eb162b3233513d8f2c853c78964", size = 6957, upload-time = "2023-07-07T09:18:24.717Z" } + [[package]] name = "tqdm" version = "4.67.1"