diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..3aabcda
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/minecraft.iml b/.idea/minecraft.iml
new file mode 100644
index 0000000..8437fe6
--- /dev/null
+++ b/.idea/minecraft.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..13bc4fb
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..577a03d
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server/.env.dev b/server/.env.dev
index e69de29..2df0093 100644
--- a/server/.env.dev
+++ b/server/.env.dev
@@ -0,0 +1,3 @@
+DATABASE_URL="sqlite+aiosqlite:///./database.sqlite3"
+JWT_ALG='HS256'
+JWT_KEY='JWT_KEY'
diff --git a/server/README.md b/server/README.md
index e7eda45..999c5e2 100644
--- a/server/README.md
+++ b/server/README.md
@@ -1 +1 @@
-pip install --index-url http://packages.webflare.ru:8888/simple --trusted-host packages.webflare.ru pyjwt
\ No newline at end of file
+pip install --index-url http://packages.webflare.ru:8888/simple --trusted-host packages.webflare.ru uv
diff --git a/server/__init__.py b/server/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/server/src/application/interface/server.py b/server/src/application/interface/server.py
new file mode 100644
index 0000000..e89d1f2
--- /dev/null
+++ b/server/src/application/interface/server.py
@@ -0,0 +1,32 @@
+from collections.abc import Sequence
+from typing import Protocol
+from uuid import UUID
+
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from server.src.application.rest.v1.schemas.server import CreateServerDTO, UpdateServerDTO, \
+ FetchListParamsDTO
+from server.src.core.database.table import Server
+
+
+class IServerService(Protocol):
+ async def fetch_list(
+ self, input_dto: FetchListParamsDTO
+ ) -> Sequence[Server]: ...
+
+
+ async def fetch_by_id(
+ self, input_dto: UUID
+ ) -> Server: ...
+
+ async def create(
+ self, input_dto: CreateServerDTO
+ ) -> Server: ...
+
+ async def update(
+ self, input_dto: tuple[UUID, UpdateServerDTO]
+ ) -> Server: ...
+
+ async def delete(
+ self, input_dto: UUID
+ ) -> None: ...
diff --git a/server/src/application/rest/di.py b/server/src/application/rest/di.py
index e69de29..3200068 100644
--- a/server/src/application/rest/di.py
+++ b/server/src/application/rest/di.py
@@ -0,0 +1,7 @@
+from server.src.application.interface.server import IServerService
+from server.src.application.use_case.server import ServerService
+from server.src.core.database.di import db
+
+
+def get_server_service() -> IServerService:
+ return ServerService(provider=db)
diff --git a/server/src/application/rest/schemas.py b/server/src/application/rest/schemas.py
new file mode 100644
index 0000000..ef5f2d2
--- /dev/null
+++ b/server/src/application/rest/schemas.py
@@ -0,0 +1,12 @@
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class BaseDTO(BaseModel):
+ model_config = ConfigDict(
+ from_attributes=True
+ )
+
+
+class PaginationDTO(BaseDTO):
+ limit: int = Field(ge=10, le=100, default=10)
+ offset: int = Field(ge=0, le=100, default=0)
diff --git a/server/src/application/rest/v1/api/server.py b/server/src/application/rest/v1/api/server.py
new file mode 100644
index 0000000..959cc60
--- /dev/null
+++ b/server/src/application/rest/v1/api/server.py
@@ -0,0 +1,81 @@
+from collections.abc import Sequence
+from http import HTTPStatus
+from uuid import UUID
+
+from fastapi import APIRouter, Depends
+from fastapi.params import Query
+
+from server.src.application.interface.server import IServerService
+from server.src.application.rest.di import get_server_service
+from server.src.application.rest.v1.schemas.server import ServerDTO, FetchListParamsDTO, CreateServerDTO, \
+ UpdateServerDTO
+
+router = APIRouter(prefix="/servers", tags=["Server"])
+
+
+@router.get(
+ path="/",
+ response_model=Sequence[ServerDTO],
+ status_code=HTTPStatus.OK
+)
+async def fetch_list(
+ params: FetchListParamsDTO = Query(),
+ service: IServerService = Depends(get_server_service)
+) -> Sequence[ServerDTO]:
+ result = service.fetch_list(input_dto=params)
+ return [ServerDTO.model_validate(result)]
+
+
+@router.get(
+ path="/{server_id}",
+ response_model=ServerDTO,
+ status_code=HTTPStatus.OK
+)
+async def fetch_by_id(
+ server_id: UUID,
+ service: IServerService = Depends(get_server_service)
+) -> ServerDTO:
+ result = service.fetch_by_id(input_dto=server_id)
+ return ServerDTO.model_validate(result)
+
+
+@router.post(
+ path="/",
+ response_model=ServerDTO,
+ status_code=HTTPStatus.CREATED
+)
+async def create(
+ create_data: CreateServerDTO,
+ service: IServerService = Depends(get_server_service)
+) -> ServerDTO:
+ result = service.create(input_dto=create_data)
+ return ServerDTO.model_validate(result)
+
+
+@router.put(
+ path="/{server_id}",
+ response_model=ServerDTO,
+ status_code=HTTPStatus.OK
+)
+async def update(
+ server_id: UUID,
+ update_data: UpdateServerDTO,
+ service: IServerService = Depends(get_server_service)
+) -> ServerDTO:
+ result = service.update(
+ input_dto=(server_id, update_data)
+ )
+ return ServerDTO.model_validate(result)
+
+
+@router.delete(
+ path="/{server_id}",
+ response_model=ServerDTO,
+ status_code=HTTPStatus.NO_CONTENT
+)
+async def delete(
+ server_id: UUID,
+ service: IServerService = Depends(get_server_service)
+) -> ServerDTO:
+ result = service.delete(input_dto=server_id)
+ return ServerDTO.model_validate(result)
diff --git a/server/src/application/rest/v1/schemas/server.py b/server/src/application/rest/v1/schemas/server.py
new file mode 100644
index 0000000..530402e
--- /dev/null
+++ b/server/src/application/rest/v1/schemas/server.py
@@ -0,0 +1,52 @@
+from enum import Enum
+from pydantic import HttpUrl, Field, IPvAnyAddress, constr
+from server.src.application.rest.schemas import BaseDTO, PaginationDTO
+
+
+class ServerStatus(Enum):
+ ONLINE = "online"
+ OFFLINE = "offline"
+
+
+class ServerDTO(BaseDTO):
+ id: int
+ img_url: HttpUrl | None
+ name: str
+ domain: str
+ ip_address: IPvAnyAddress
+ port: int
+ version: str
+ max_players: int
+ online_players: int
+ status: ServerStatus
+ type: str
+
+
+class CreateServerDTO(BaseDTO):
+ img_url: HttpUrl | None = Field(default=None)
+ name: str = Field(min_length=5, max_length=100)
+ domain: str
+ ip_address: IPvAnyAddress
+ port: int = Field(default=25565)
+ version: constr(pattern=r'^\d+\.\d+(\.\d+)?$')
+ max_players: int = Field(ge=1)
+ online_players: int = Field(ge=0)
+ status: ServerStatus = Field(default=ServerStatus.ONLINE)
+ type: str
+
+
+class UpdateServerDTO(BaseDTO):
+ img_url: HttpUrl | None = Field(default=None)
+ name: str | None = Field(min_length=5, max_length=100)
+ domain: str | None
+ ip_address: IPvAnyAddress | None
+ port: int | None
+ version: constr(pattern=r'^\d+\.\d+(\.\d+)?$') | None
+ max_players: int = Field(ge=1) | None
+ online_players: int = Field(ge=0) | None
+ status: ServerStatus | None
+ type: str | None
+
+
+class FetchListParamsDTO(PaginationDTO):
+ pass
diff --git a/server/src/application/use_case/server.py b/server/src/application/use_case/server.py
new file mode 100644
index 0000000..ec581c1
--- /dev/null
+++ b/server/src/application/use_case/server.py
@@ -0,0 +1,68 @@
+from collections.abc import Sequence
+from uuid import UUID
+from sqlalchemy import select, delete, insert, update
+
+from server.src.application.interface.server import IServerService
+from server.src.application.rest.v1.schemas.server import CreateServerDTO, UpdateServerDTO, \
+ FetchListParamsDTO
+from server.src.core.database.di import DBProvaider
+from server.src.core.database.table import Server
+
+
+class ServerService(IServerService):
+
+ def __init__(
+ self, provider: DBProvaider
+ ) -> None:
+ self.session = provider.session
+
+ async def fetch_list(
+ self, input_dto: FetchListParamsDTO
+ ) -> Sequence[Server]:
+ query = (
+ select(Server)
+ .offset(input_dto.offset)
+ .limit(input_dto.limit)
+ )
+ return (await self.session.scalars(query)).all()
+
+ async def fetch_by_id(
+ self, input_dto: UUID
+ ) -> Server:
+ query = (
+ select(Server)
+ .where(Server.id == input_dto)
+ )
+ return (await self.session.scalars(query)).one()
+
+ async def create(
+ self, input_dto: CreateServerDTO
+ ) -> Server:
+ stmt = (
+ insert(Server)
+ .values(
+ input_dto.model_dump()
+ )
+ .returning(Server)
+ )
+ return (await self.session.scalars(stmt)).one()
+
+ async def update(
+ self, input_dto: tuple[UUID, UpdateServerDTO]
+ ) -> Server:
+ server_id, update_data = input_dto
+ stmt = (
+ update(Server)
+ .where(Server.id == server_id)
+ .values(**update_data.model_dump(
+ exclude_unset=True
+ ))
+ .returning(Server)
+ )
+ return (await self.session.scalars(stmt)).one()
+
+ async def delete(
+ self, input_dto: UUID
+ ) -> None:
+ stmt = delete(Server).where(Server.id == input_dto)
+ await self.session.execute(stmt)
diff --git a/server/src/core/config.py b/server/src/core/config.py
index e69de29..d46feaf 100644
--- a/server/src/core/config.py
+++ b/server/src/core/config.py
@@ -0,0 +1,14 @@
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(
+ env_file=".env",
+ env_file_encoding="utf-8",
+ extra="ignore"
+ )
+ DATABASE_URL: str ="sqlite+aiosqlite:///./database.sqlite3"
+ JWT_ALG: str ='HS256'
+ JWT_KEY: str ='JWT_KEY'
+
+settings = Settings()
diff --git a/server/src/core/database/di.py b/server/src/core/database/di.py
index e69de29..c5a255a 100644
--- a/server/src/core/database/di.py
+++ b/server/src/core/database/di.py
@@ -0,0 +1,27 @@
+from src.core.database.utils import Database
+from src.core.config import settings, Settings
+
+class DBProvaider:
+ serves: Database = Database()
+ settings: Settings = settings
+
+ serves.create_engine(
+ settings.DATABASE_URL,
+ )
+
+ serves.create_session()
+
+ session = serves.session
+ async def get_session(self):
+ async with self.session as session:
+ try:
+ yield session
+ await session.flush()
+ except Exception as e:
+ await session.rollback()
+ raise e
+ finally:
+ await session.close()
+
+
+db = DBProvaider()
diff --git a/server/src/core/database/table.py b/server/src/core/database/table.py
index 66a9ab6..2223478 100644
--- a/server/src/core/database/table.py
+++ b/server/src/core/database/table.py
@@ -1,17 +1,37 @@
-from uuid import UUID
from uuid import uuid4
from datetime import datetime
+from uuid import UUID
+from sqlalchemy import UUID as SQL_UUID, DateTime, String, Integer, Enum
+from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped, declared_attr
+
+from server.src.application.rest.v1.schemas.server import ServerStatus
+
class TableSQLBase(DeclarativeBase):
- id: Mapped[UUID] = mapped_column(UUID, default=uuid4(), unqiue=True, primery_key=True)
+ id: Mapped[UUID] = mapped_column(SQL_UUID, default=uuid4(), unqiue=True, primery_key=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now(), nuleble=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, onupdate=datetime.now(), nuleble=True)
- @getattr
- def __tablename(cls):
- return f"{cls.__name__.lover()}s"
+ @declared_attr
+ def __tablename__(cls):
+ return f"{cls.__name__.lower()}s"
class User(TableSQLBase):
username: Mapped[str] = mapped_column(String(50), nuleble=False)
password: Mapped[str] = mapped_column(String(255), nuleble=False)
+
+
+class Server(TableSQLBase):
+ class Server(TableSQLBase):
+ img_url: Mapped[str | None] = mapped_column(String(500))
+ name: Mapped[str] = mapped_column(String(100))
+ ip_address: Mapped[str] = mapped_column(String(45))
+ port: Mapped[int] = mapped_column(Integer, default=25565)
+ version: Mapped[str] = mapped_column(String(20))
+ max_players: Mapped[int] = mapped_column(Integer, default=20)
+ online_players: Mapped[int] = mapped_column(Integer, default=0)
+ status: Mapped[ServerStatus] = mapped_column(
+ Enum(ServerStatus), default=ServerStatus.OFFLINE
+ )
+ type: Mapped[str] = mapped_column(String(50), nullable=False)
diff --git a/server/src/core/database/utils.py b/server/src/core/database/utils.py
index e69de29..b55bf13 100644
--- a/server/src/core/database/utils.py
+++ b/server/src/core/database/utils.py
@@ -0,0 +1,25 @@
+from sqlalchemy.ext.asyncio import (
+ create_async_engine, async_sessionmaker, AsyncSession, AsyncEngine
+)
+
+
+class Database:
+ engine: AsyncEngine
+ session: AsyncSession
+
+ def engine(self,
+ url: str,
+ echo: bool=False
+ ):
+ self.engine = create_async_engine(
+ url,
+ echo=echo
+ )
+
+ def session(self, autoflush:bool = False):
+ self.session = async_sessionmaker(
+ self.engine,
+ class_=AsyncSession,
+ autoflush=autoflush
+ )
+