From 9b857e5caa2d675d83d0d0b6de0b5630d92ad1fe Mon Sep 17 00:00:00 2001 From: itqop Date: Sat, 18 Oct 2025 15:11:10 +0300 Subject: [PATCH] fixes --- src/hubgw/api/v1/luckperms.py | 20 ++++- src/hubgw/context.py | 109 ++++++++++++++++------- src/hubgw/core/config.py | 3 +- src/hubgw/repositories/luckperms_repo.py | 10 ++- src/hubgw/schemas/luckperms.py | 9 +- src/hubgw/services/luckperms_service.py | 89 ++++++------------ 6 files changed, 143 insertions(+), 97 deletions(-) diff --git a/src/hubgw/api/v1/luckperms.py b/src/hubgw/api/v1/luckperms.py index 1d6331e..cd38019 100644 --- a/src/hubgw/api/v1/luckperms.py +++ b/src/hubgw/api/v1/luckperms.py @@ -5,7 +5,10 @@ from typing import Annotated from hubgw.api.deps import get_luckperms_service, verify_api_key from hubgw.services.luckperms_service import LuckPermsService -from hubgw.schemas.luckperms import LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission, LuckPermsPlayerWithPermissions +from hubgw.schemas.luckperms import ( + LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission, + LuckPermsPlayerWithPermissions, LuckPermsPlayerCreateRequest +) from hubgw.core.errors import AppError, create_http_exception router = APIRouter() @@ -73,4 +76,17 @@ async def get_player_with_permissions( try: return await service.get_player_with_permissions(uuid) except AppError as e: - raise create_http_exception(e) \ No newline at end of file + raise create_http_exception(e) + + +@router.post("/players", response_model=LuckPermsPlayer, status_code=201) +async def create_player( + request: LuckPermsPlayerCreateRequest, + service: Annotated[LuckPermsService, Depends(get_luckperms_service)], + _: Annotated[str, Depends(verify_api_key)] +): + """Create a new player in LuckPerms.""" + try: + return await service.create_player(request) + except AppError as e: + raise create_http_exception(e) diff --git a/src/hubgw/context.py b/src/hubgw/context.py index b535a0a..d9ce482 100644 --- a/src/hubgw/context.py +++ b/src/hubgw/context.py @@ -1,40 +1,85 @@ """Application context singleton.""" -from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine +from collections.abc import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.pool import NullPool + from hubgw.core.config import APP_CONFIG +from hubgw.models.base import Base -class AppContext: - """Application context singleton.""" - - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +class AppContext(metaclass=Singleton): def __init__(self): - if not hasattr(self, 'initialized'): - self.settings = APP_CONFIG - self.engine: AsyncEngine = None - self.session_factory: async_sessionmaker = None - self.initialized = True - + self.settings = APP_CONFIG + self._engine = None + self._session_factory = None + + @property + def engine(self): + if self._engine is None: + connect_args = { + "statement_cache_size": 0, + "timeout": 20, + } + engine_kwargs = { + "connect_args": connect_args, + "pool_pre_ping": True, + } + + self._engine = create_async_engine( + self.settings.database.dsn, + pool_size=self.settings.database.pool_size, + max_overflow=self.settings.database.max_overflow, + pool_recycle=True, + echo=self.settings.database.echo, + **engine_kwargs, + ) + + Base.metadata.bind = self._engine + + return self._engine + + @property + def session_factory(self) -> async_sessionmaker[AsyncSession]: + if self._session_factory is None: + self._session_factory = async_sessionmaker( + bind=self.engine, + class_=AsyncSession, + expire_on_commit=False, + ) + return self._session_factory + + async def get_db_session(self) -> AsyncGenerator[AsyncSession, None]: + session = self.session_factory() + try: + yield session + finally: + await session.close() + async def startup(self): - """Initialize database engine and session factory.""" - self.engine = create_async_engine( - self.settings.database.dsn, - pool_size=self.settings.database.pool_size, - max_overflow=self.settings.database.max_overflow, - echo=self.settings.database.echo - ) - self.session_factory = async_sessionmaker( - self.engine, - expire_on_commit=False - ) - + pass + async def shutdown(self): - """Close database engine.""" - if self.engine: - await self.engine.dispose() + try: + if self._engine is not None: + await self._engine.dispose() + self._engine = None + except Exception as e: + print(f"Error disposing engine: {e}") + + +APP_CTX = AppContext() + +__all__ = [ + "APP_CTX", +] \ No newline at end of file diff --git a/src/hubgw/core/config.py b/src/hubgw/core/config.py index 1007bf2..ef459d4 100644 --- a/src/hubgw/core/config.py +++ b/src/hubgw/core/config.py @@ -4,6 +4,7 @@ from dotenv import load_dotenv from pydantic import Field, computed_field from pydantic_settings import BaseSettings from typing import Optional +from urllib.parse import quote_plus load_dotenv() @@ -63,7 +64,7 @@ class DatabaseSettings(BaseSettings): def dsn(self) -> str: """Generate database DSN from connection parameters.""" return ( - f"postgresql+asyncpg://{self.user}:{self.password}" + f"postgresql+asyncpg://{self.user}:{quote_plus(self.password)}" f"@{self.host}:{self.port}/{self.database}" ) diff --git a/src/hubgw/repositories/luckperms_repo.py b/src/hubgw/repositories/luckperms_repo.py index 2c55bd7..4349594 100644 --- a/src/hubgw/repositories/luckperms_repo.py +++ b/src/hubgw/repositories/luckperms_repo.py @@ -35,4 +35,12 @@ class LuckPermsRepository: """Get user permissions.""" stmt = select(LuckPermsUserPermission).where(LuckPermsUserPermission.uuid == uuid) result = await self.session.execute(stmt) - return list(result.scalars().all()) \ No newline at end of file + return list(result.scalars().all()) + + async def create_player(self, uuid: str, username: str, primary_group: str) -> LuckPermsPlayer: + """Create a new player in LuckPerms.""" + player = LuckPermsPlayer(uuid=uuid, username=username, primary_group=primary_group) + self.session.add(player) + await self.session.commit() + await self.session.refresh(player) + return player \ No newline at end of file diff --git a/src/hubgw/schemas/luckperms.py b/src/hubgw/schemas/luckperms.py index 5ea1a41..25595de 100644 --- a/src/hubgw/schemas/luckperms.py +++ b/src/hubgw/schemas/luckperms.py @@ -37,4 +37,11 @@ class LuckPermsUserPermission(BaseSchema): class LuckPermsPlayerWithPermissions(LuckPermsPlayer): """LuckPerms Player with permissions schema.""" - permissions: list[LuckPermsUserPermission] = [] \ No newline at end of file + permissions: list[LuckPermsUserPermission] = [] + + +class LuckPermsPlayerCreateRequest(BaseModel): + """Request schema for creating a LuckPerms player.""" + + username: str + primary_group: str = "default" \ No newline at end of file diff --git a/src/hubgw/services/luckperms_service.py b/src/hubgw/services/luckperms_service.py index 2bf899b..b4dff2f 100644 --- a/src/hubgw/services/luckperms_service.py +++ b/src/hubgw/services/luckperms_service.py @@ -1,11 +1,15 @@ """LuckPerms service.""" from sqlalchemy.ext.asyncio import AsyncSession -from typing import List, Optional +from typing import List +from uuid import uuid3, NAMESPACE_OID from hubgw.repositories.luckperms_repo import LuckPermsRepository -from hubgw.schemas.luckperms import LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission, LuckPermsPlayerWithPermissions -from hubgw.core.errors import NotFoundError +from hubgw.schemas.luckperms import ( + LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission, + LuckPermsPlayerWithPermissions, LuckPermsPlayerCreateRequest +) +from hubgw.core.errors import NotFoundError, AlreadyExistsError class LuckPermsService: @@ -19,56 +23,26 @@ class LuckPermsService: player = await self.repo.get_player(uuid) if not player: raise NotFoundError(f"Player {uuid} not found") - return LuckPermsPlayer( - uuid=player.uuid, - username=player.username, - primary_group=player.primary_group, - created_at=player.created_at, - updated_at=player.updated_at - ) + return LuckPermsPlayer.model_validate(player) async def get_player_by_username(self, username: str) -> LuckPermsPlayer: """Get player by username.""" player = await self.repo.get_player_by_username(username) if not player: raise NotFoundError(f"Player {username} not found") - return LuckPermsPlayer( - uuid=player.uuid, - username=player.username, - primary_group=player.primary_group, - created_at=player.created_at, - updated_at=player.updated_at - ) + return LuckPermsPlayer.model_validate(player) async def get_group(self, name: str) -> LuckPermsGroup: """Get group by name.""" group = await self.repo.get_group(name) if not group: raise NotFoundError(f"Group {name} not found") - return LuckPermsGroup( - name=group.name, - created_at=group.created_at, - updated_at=group.updated_at - ) + return LuckPermsGroup.model_validate(group) async def get_user_permissions(self, uuid: str) -> List[LuckPermsUserPermission]: """Get user permissions.""" permissions = await self.repo.get_user_permissions(uuid) - return [ - LuckPermsUserPermission( - id=perm.id, - uuid=perm.uuid, - permission=perm.permission, - value=perm.value, - server=perm.server, - world=perm.world, - expiry=perm.expiry, - contexts=perm.contexts, - created_at=perm.created_at, - updated_at=perm.updated_at - ) - for perm in permissions - ] + return [LuckPermsUserPermission.model_validate(perm) for perm in permissions] async def get_player_with_permissions(self, uuid: str) -> LuckPermsPlayerWithPermissions: """Get player with permissions.""" @@ -78,27 +52,22 @@ class LuckPermsService: permissions = await self.repo.get_user_permissions(uuid) - permission_schemas = [ - LuckPermsUserPermission( - id=perm.id, - uuid=perm.uuid, - permission=perm.permission, - value=perm.value, - server=perm.server, - world=perm.world, - expiry=perm.expiry, - contexts=perm.contexts, - created_at=perm.created_at, - updated_at=perm.updated_at - ) - for perm in permissions - ] + permission_schemas = [LuckPermsUserPermission.model_validate(perm) for perm in permissions] - return LuckPermsPlayerWithPermissions( - uuid=player.uuid, - username=player.username, - primary_group=player.primary_group, - created_at=player.created_at, - updated_at=player.updated_at, - permissions=permission_schemas - ) \ No newline at end of file + return LuckPermsPlayerWithPermissions.model_validate(player, permissions=permission_schemas) + + async def create_player(self, request: LuckPermsPlayerCreateRequest) -> LuckPermsPlayer: + """Create a new player in LuckPerms.""" + existing_player = await self.repo.get_player_by_username(request.username) + if existing_player: + raise AlreadyExistsError(f"Player with username {request.username} already exists") + + uuid = str(uuid3(NAMESPACE_OID, request.username)) + + created_player = await self.repo.create_player( + uuid=uuid, + username=request.username, + primary_group=request.primary_group + ) + + return LuckPermsPlayer.model_validate(created_player)