Compare commits

..
8 Commits
Author SHA1 Message Date
ciomek 2fee340259 Fixes
Build and Push Docker Image / build-and-push (push) Successful in 15s
2026-09-04 23:50:58 +02:00
ciomek 9964fbf044 No SQLModel 2026-09-04 23:41:35 +02:00
ciomek 5f0e8c11b4 User filter
Build and Push Docker Image / build-and-push (push) Successful in 19s
2026-09-04 18:03:21 +02:00
ciomek 655fb89a7f Users
Build and Push Docker Image / build-and-push (push) Successful in 27s
2026-09-04 17:52:08 +02:00
ciomek 632d990c85 Player format 2026-09-04 17:47:36 +02:00
ciomek 3dcd4173c2 Fixes 2026-09-04 17:41:24 +02:00
ciomek dbed39ee3a MC Users 2026-09-04 17:31:30 +02:00
ciomek 5732e26380 Logs 2026-09-04 17:02:25 +02:00
27 changed files with 425 additions and 101 deletions

No files matched your search

+2 -3
View File
@@ -4,9 +4,8 @@ from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from sqlmodel import SQLModel
from loon.web.db import database_url
from loon.web.db import Base, database_url
import loon.web
@@ -23,7 +22,7 @@ if config.config_file_name is not None:
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata
target_metadata = Base.metadata
config.set_main_option("sqlalchemy.url", database_url())
-2
View File
@@ -10,8 +10,6 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
-42
View File
@@ -1,42 +0,0 @@
"""Users
Revision ID: 2df7e2c6af43
Revises:
Create Date: 2026-08-17 12:42:05.103510
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = '2df7e2c6af43'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('internal_username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('internal_username'),
sa.UniqueConstraint('uuid')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###
+65
View File
@@ -0,0 +1,65 @@
"""init
Revision ID: 7efb7d0c39ae
Revises:
Create Date: 2026-09-04 23:50:22.513272
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7efb7d0c39ae'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('logtype',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.create_table('minecraftuser',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid')
)
op.create_table('logentry',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('log_type_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('message', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['log_type_id'], ['logtype.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('minecraft_user_id', sa.Integer(), nullable=False),
sa.Column('internal_username', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['minecraft_user_id'], ['minecraftuser.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('internal_username'),
sa.UniqueConstraint('minecraft_user_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
op.drop_table('logentry')
op.drop_table('minecraftuser')
op.drop_table('logtype')
# ### end Alembic commands ###
+13 -7
View File
@@ -1,10 +1,13 @@
import json
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.users.models import User
from loon.web.logs.service import write_log, player_tag
from loon.web.logs.types import LogTypeEnum
from loon.web.users.models import MinecraftUser, User
async def change_password_handler(client, userdata, msg, data):
@@ -22,18 +25,19 @@ async def change_password_handler(client, userdata, msg, data):
password = data["password"]
with Session(engine) as session:
statement = select(User).where(User.uuid == uuid)
user = session.exec(statement).first()
statement = select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
user = session.execute(statement).scalars().first()
if user is None:
client.publish(f"loon/auth/change_password/{uuid}/response",
json.dumps({"success": False, "error": "The user is not registered!"}))
return
user = session.exec(
select(User).where(User.uuid == uuid)
).first()
user = session.execute(
select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
).scalars().first()
user.password = password
internal_username = user.internal_username
try:
session.commit()
@@ -44,4 +48,6 @@ async def change_password_handler(client, userdata, msg, data):
return
await write_log(LogTypeEnum.PLAYER_PASSWORD_CHANGED, f"Player {player_tag(uuid, internal_username)} changed their password.")
client.publish(f"loon/auth/change_password/{uuid}/response", json.dumps({"success": True, "error": None}))
+29 -1
View File
@@ -1,10 +1,14 @@
import json
import random
from loon.web.logs.service import write_log, player_tag
from loon.web.logs.types import LogTypeEnum
from loon.web.users.state import user_threads
PLAYER_EVENT_SUFFIXES = ("died", "joined", "left", "kicked")
BAN_KICK_REASON = "Banned by admin"
DEATH_BY_DAMAGE = {
"fall": (
"{username} fell from a high place",
@@ -78,7 +82,8 @@ async def marquee_handler(client, userdata, msg, data):
if not isinstance(player, dict):
return
username = player.get("username")
if not username:
uuid = player.get("uuid")
if not username or not uuid:
return
if subtopic == "died":
@@ -88,20 +93,43 @@ async def marquee_handler(client, userdata, msg, data):
if killer:
templates = DEATH_BY_KILLER.get(damage_type, DEATH_BY_KILLER_DEFAULT)
message = get_random(templates, username=username, killer=killer)
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} was killed by {killer}.")
elif damage_type in DEATH_BY_DAMAGE:
message = get_random(DEATH_BY_DAMAGE[damage_type], username=username)
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} died of {damage_type}.")
elif damage_type:
message = f"{username} died of {damage_type}"
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} died of {damage_type}.")
else:
message = f"{username} died"
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} died.")
elif subtopic == "joined":
message = f"{username} joined the game"
await write_log(LogTypeEnum.PLAYER_JOINED, f"Player {player_tag(uuid, username)} joined the game.")
elif subtopic == "kicked":
reason = data.get("reason")
if reason == BAN_KICK_REASON:
message = f"{username} was banned"
await write_log(LogTypeEnum.PLAYER_BANNED, f"Player {player_tag(uuid, username)} was banned.")
else:
message = f"{username} was kicked: {reason}" if reason else f"{username} was kicked"
await write_log(
LogTypeEnum.PLAYER_KICKED,
f"Player {player_tag(uuid, username)} was kicked: {reason}." if reason else f"Player {player_tag(uuid, username)} was kicked.",
)
else:
message = f"{username} left the game"
await write_log(LogTypeEnum.PLAYER_LEFT, f"Player {player_tag(uuid, username)} left the game.")
envelope = json.dumps({"topic": "server/marquee", "payload": {"message": message}})
for queue in list(user_threads.values()):
+12
View File
@@ -0,0 +1,12 @@
from loon.web.users.service import sync_minecraft_users
async def player_joined_handler(client, userdata, msg, data):
if msg.topic != "loon/playerlist/joined":
return
player = data.get("player")
if not player:
return
await sync_minecraft_users([{"uuid": player["uuid"], "username": player["username"]}])
+8
View File
@@ -0,0 +1,8 @@
from loon.web.users.service import sync_minecraft_users
async def players_sync_handler(client, userdata, msg, data):
if msg.topic != "loon/playerlist/sync":
return
await sync_minecraft_users(data.get("players", []))
+18 -6
View File
@@ -1,10 +1,13 @@
import json
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.users.models import User
from loon.web.logs.service import write_log, player_tag
from loon.web.logs.types import LogTypeEnum
from loon.web.users.models import MinecraftUser, User
async def register_request_handler(client, userdata, msg, data):
@@ -23,8 +26,15 @@ async def register_request_handler(client, userdata, msg, data):
internal_username = data["internalUsername"]
with Session(engine) as session:
statement = select(User).where(User.uuid == uuid)
user = session.exec(statement).first()
minecraft_user = session.execute(select(MinecraftUser).where(MinecraftUser.uuid == uuid)).scalars().first()
if minecraft_user is None:
client.publish(f"loon/auth/register/{uuid}/response",
json.dumps({"success": False, "error": "Player not recognized yet, please rejoin the server and try again."}))
return
statement = select(User).where(User.minecraft_user_id == minecraft_user.id)
user = session.execute(statement).scalars().first()
if user is not None:
client.publish(f"loon/auth/register/{uuid}/response",
@@ -32,14 +42,14 @@ async def register_request_handler(client, userdata, msg, data):
return
statement = select(User).where(User.internal_username == internal_username)
user = session.exec(statement).first()
user = session.execute(statement).scalars().first()
if user is not None:
client.publish(f"loon/auth/register/{uuid}/response",
json.dumps({"success": False, "error": f"The {internal_username} is taken!"}))
return
user = User(uuid=uuid, password=password, internal_username=internal_username, admin=False)
user = User(minecraft_user_id=minecraft_user.id, password=password, internal_username=internal_username, admin=False)
session.add(user)
try:
session.commit()
@@ -50,4 +60,6 @@ async def register_request_handler(client, userdata, msg, data):
return
await write_log(LogTypeEnum.PLAYER_REGISTERED, f"Player {player_tag(uuid, internal_username)} registered.")
client.publish(f"loon/auth/register/{uuid}/response", json.dumps({"success": True, "error": None}))
+6 -3
View File
@@ -1,9 +1,10 @@
import json
from sqlmodel import Session, select
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.users.models import User
from loon.web.users.models import MinecraftUser, User
async def show_username_handler(client, userdata, msg, data):
@@ -19,7 +20,9 @@ async def show_username_handler(client, userdata, msg, data):
return
with Session(engine) as session:
user = session.exec(select(User).where(User.uuid == uuid)).first()
user = session.execute(
select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
).scalars().first()
if user is None:
client.publish(f"loon/auth/show_username/{uuid}/response",
+5 -1
View File
@@ -9,6 +9,8 @@ from loon import logger
from loon.mqtt.handlers.change_password_request import change_password_handler
from loon.mqtt.handlers.marquee import marquee_handler
from loon.mqtt.handlers.player import player_handler
from loon.mqtt.handlers.player_joined import player_joined_handler
from loon.mqtt.handlers.players_sync import players_sync_handler
from loon.mqtt.handlers.register_request import register_request_handler
from loon.mqtt.handlers.server import server_handler
from loon.mqtt.handlers.show_username_request import show_username_handler
@@ -16,7 +18,7 @@ from loon.mqtt.handlers.world import world_handler
class MQTTManager:
BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler, show_username_handler]
BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler, show_username_handler, players_sync_handler, player_joined_handler]
def __init__(self, inject_handlers = None):
if inject_handlers is None:
@@ -53,6 +55,8 @@ class MQTTManager:
logger.info(f"MQTT connected (rc={reason_code})")
client.subscribe("loon/#")
client.publish("loon/playerlist/sync/request")
@staticmethod
def on_disconnect(client, userdata, reason_code):
logger.warning(f"MQTT disconnected (rc={reason_code}). Reconnecting...")
+4 -3
View File
@@ -1,4 +1,5 @@
from sqlmodel import Session, select
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.users.models import User
@@ -6,9 +7,9 @@ from loon.web.users.models import User
def choose_admin(username: str) -> None:
with Session(engine) as session:
user = session.exec(
user = session.execute(
select(User).where(User.internal_username == username)
).first()
).scalars().first()
if user is None:
print(f"No user named {username!r}.")
+79 -1
View File
@@ -1,7 +1,85 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from sqlalchemy.orm import selectinload, Session
from sqlalchemy import select
from starlette.requests import Request
from loon.web.auth.middleware import admin
from loon.web.db import engine
from loon.web.logs.models import LogEntry
from loon.web.logs.schema import LogEntryResponse
from loon.web.users.models import MinecraftUser, User
from loon.web.users.schema import MinecraftUserPublic, UserPublic
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/logs", response_model=list[LogEntryResponse])
@admin
async def list_logs(request: Request):
with Session(engine) as session:
statement = (
select(LogEntry)
.options(selectinload(LogEntry.log_type))
.order_by(LogEntry.created_at)
)
entries = session.execute(statement).scalars().all()
return [
LogEntryResponse(
id=entry.id,
log_type=entry.log_type.code,
created_at=entry.created_at,
message=entry.message,
)
for entry in entries
]
@router.get("/mc-users", response_model=list[MinecraftUserPublic])
@admin
async def list_mc_users(request: Request):
with Session(engine) as session:
statement = select(MinecraftUser).order_by(MinecraftUser.username)
return session.execute(statement).scalars().all()
@router.get("/users", response_model=list[UserPublic])
@admin
async def list_users(request: Request):
with Session(engine) as session:
statement = (
select(User)
.options(selectinload(User.minecraft_user))
.order_by(User.internal_username)
)
return session.execute(statement).scalars().all()
@router.get("/users/{internal_username}", response_model=UserPublic)
@admin
async def get_user(request: Request, internal_username: str):
with Session(engine) as session:
statement = (
select(User)
.where(User.internal_username == internal_username)
.options(selectinload(User.minecraft_user))
)
user = session.execute(statement).scalars().first()
if user is None:
raise HTTPException(status_code=404)
return user
@router.get("/users/by-uuid/{uuid}", response_model=UserPublic)
@admin
async def get_user_by_uuid(request: Request, uuid: str):
with Session(engine) as session:
statement = (
select(User)
.join(MinecraftUser)
.where(MinecraftUser.uuid == uuid)
.options(selectinload(User.minecraft_user))
)
user = session.execute(statement).scalars().first()
if user is None:
raise HTTPException(status_code=404)
return user
+5 -2
View File
@@ -8,7 +8,8 @@ from starlette.authentication import (
)
from starlette.requests import HTTPConnection
from starlette.responses import JSONResponse, Response
from sqlmodel import Session, select
from sqlalchemy.orm import selectinload, Session
from sqlalchemy import select
from loon.web.db import engine
from loon.web.users.models import User
@@ -62,7 +63,9 @@ class JWTAuthBackend(AuthenticationBackend):
raise AuthenticationError("Invalid or expired token")
with Session(engine) as session:
user = session.exec(select(User).where(User.id == user_id)).first()
user = session.execute(
select(User).where(User.id == user_id).options(selectinload(User.minecraft_user))
).scalars().first()
if user is None:
raise AuthenticationError("User not found")
+3 -2
View File
@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException, Request, status
from sqlmodel import Session, select
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web import limiter
from loon.web.auth.middleware import authenticated
@@ -17,7 +18,7 @@ router = APIRouter(prefix="/auth", tags=["auth"])
async def get_jwt(request: Request, body: JWTRequest) -> JWTResponse:
with Session(engine) as session:
statement = select(User).where(User.internal_username == body.username)
user = session.exec(statement).first()
user = session.execute(statement).scalars().first()
if user is None or user.id is None or not verify_password(body.password, user.password):
raise HTTPException(
+6 -1
View File
@@ -1,8 +1,13 @@
from sqlmodel import SQLModel, create_engine
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase
from config import settings
class Base(DeclarativeBase):
pass
def database_url(url: str | None = None) -> str:
url = url or settings.database.url
for src, dst in (
View File
Whitespace-only changes.
+24
View File
@@ -0,0 +1,24 @@
from datetime import datetime, timezone
from sqlalchemy import DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from loon.web.db import Base
class LogType(Base):
__tablename__ = "logtype"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[str] = mapped_column(unique=True)
entries: Mapped[list["LogEntry"]] = relationship(back_populates="log_type")
class LogEntry(Base):
__tablename__ = "logentry"
id: Mapped[int] = mapped_column(primary_key=True)
log_type_id: Mapped[int] = mapped_column(ForeignKey("logtype.id"))
log_type: Mapped[LogType] = relationship(back_populates="entries")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
message: Mapped[str]
+10
View File
@@ -0,0 +1,10 @@
from datetime import datetime
from pydantic import BaseModel
class LogEntryResponse(BaseModel):
id: int
log_type: str
created_at: datetime
message: str
+32
View File
@@ -0,0 +1,32 @@
import asyncio
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.logs.models import LogType, LogEntry
from loon.web.logs.types import LogTypeEnum
def player_tag(uuid: str, username: str) -> str:
return f"[player:{uuid}]{username}[/player]"
def _write_log(log_type: LogTypeEnum, message: str) -> None:
with Session(engine) as session:
statement = select(LogType).where(LogType.code == log_type.value)
log_type_object = session.execute(statement).scalars().first()
if not log_type_object:
log_type_object = LogType(code=log_type.value)
session.add(log_type_object)
session.commit()
session.refresh(log_type_object)
log_entry = LogEntry(log_type_id=log_type_object.id, message=message)
session.add(log_entry)
session.commit()
async def write_log(log_type: LogTypeEnum, message: str) -> None:
await asyncio.to_thread(_write_log, log_type, message)
+11
View File
@@ -0,0 +1,11 @@
from enum import Enum
class LogTypeEnum(Enum):
PLAYER_DEAD = "player_dead"
PLAYER_JOINED = "player_joined"
PLAYER_LEFT = "player_left"
PLAYER_KICKED = "player_kicked"
PLAYER_BANNED = "player_banned"
PLAYER_REGISTERED = "player_registered"
PLAYER_PASSWORD_CHANGED = "player_password_changed"
+28 -7
View File
@@ -1,9 +1,30 @@
from sqlmodel import SQLModel, Field
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from loon.web.db import Base
class User(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
uuid: str = Field(unique=True)
internal_username: str = Field(unique=True)
password: str
admin: bool = Field(default=False)
class MinecraftUser(Base):
__tablename__ = "minecraftuser"
id: Mapped[int] = mapped_column(primary_key=True)
uuid: Mapped[str] = mapped_column(unique=True)
username: Mapped[str]
user: Mapped["User"] = relationship(back_populates="minecraft_user")
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
minecraft_user_id: Mapped[int] = mapped_column(ForeignKey("minecraftuser.id"), unique=True)
minecraft_user: Mapped[MinecraftUser] = relationship(back_populates="user")
internal_username: Mapped[str] = mapped_column(unique=True)
password: Mapped[str]
admin: Mapped[bool] = mapped_column(default=False)
@property
def uuid(self) -> str:
return self.minecraft_user.uuid
+5 -3
View File
@@ -3,15 +3,17 @@ from starlette.websockets import WebSocket, WebSocketDisconnect
from loon.web import get_mqtt_manager
from loon.web.auth.middleware import authenticated
from loon.web.users.models import User
from loon.web.users.schema import UserPublic
from loon.web.users.state import subscribe_player, get_user_messages, unsubscribe_player
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/me")
@router.get("/me", response_model=UserPublic)
@authenticated
async def me(request: Request):
return request.user.user.model_dump(exclude={"password", "id"})
async def me(request: Request) -> User:
return request.user.user
@router.post("/inventory/request")
@authenticated
+16
View File
@@ -0,0 +1,16 @@
from pydantic import BaseModel, ConfigDict
class MinecraftUserPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: str
username: str
class UserPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
internal_username: str
admin: bool
minecraft_user: MinecraftUserPublic
+44
View File
@@ -0,0 +1,44 @@
import asyncio
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.users.models import MinecraftUser
def _sync_minecraft_users(players: list[dict]) -> list[MinecraftUser]:
players_by_uuid = {player["uuid"]: player["username"] for player in players}
with Session(engine) as session:
existing = session.execute(
select(MinecraftUser).where(MinecraftUser.uuid.in_(players_by_uuid))
).scalars().all()
existing_by_uuid = {minecraft_user.uuid: minecraft_user for minecraft_user in existing}
new_users = []
for uuid, username in players_by_uuid.items():
minecraft_user = existing_by_uuid.get(uuid)
if minecraft_user is None:
minecraft_user = MinecraftUser(uuid=uuid, username=username)
new_users.append(minecraft_user)
session.add(minecraft_user)
elif minecraft_user.username != username:
minecraft_user.username = username
session.add(minecraft_user)
session.commit()
minecraft_users = [*existing, *new_users]
for minecraft_user in minecraft_users:
session.refresh(minecraft_user)
return minecraft_users
async def sync_minecraft_users(players: list[dict]) -> list[MinecraftUser]:
if not players:
return []
return await asyncio.to_thread(_sync_minecraft_users, players)
-1
View File
@@ -55,7 +55,6 @@ dependencies = [
"shellingham==1.5.4",
"slowapi>=0.1.10",
"sqlalchemy==2.0.51",
"sqlmodel==0.0.39",
"starlette==1.3.1",
"typer==0.27.0",
"typing-extensions==4.16.0",
Generated
-16
View File
@@ -516,7 +516,6 @@ dependencies = [
{ name = "shellingham" },
{ name = "slowapi" },
{ name = "sqlalchemy" },
{ name = "sqlmodel" },
{ name = "starlette" },
{ name = "typer" },
{ name = "typing-extensions" },
@@ -580,7 +579,6 @@ requires-dist = [
{ name = "shellingham", specifier = "==1.5.4" },
{ name = "slowapi", specifier = ">=0.1.10" },
{ name = "sqlalchemy", specifier = "==2.0.51" },
{ name = "sqlmodel", specifier = "==0.0.39" },
{ name = "starlette", specifier = "==1.3.1" },
{ name = "typer", specifier = "==0.27.0" },
{ name = "typing-extensions", specifier = "==4.16.0" },
@@ -1022,20 +1020,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
]
[[package]]
name = "sqlmodel"
version = "0.0.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/ee/22a0559283c3cf6048678e787ed5d4959dcd00dedd8ba4567eeae684eeb1/sqlmodel-0.0.39.tar.gz", hash = "sha256:23d8e50a8d8ee936032ed79c55023a5d618dd6bc3c510bbf4909d1a7a605a570", size = 91057, upload-time = "2026-06-25T13:01:38.475Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/7d/b9813a582d4eb310be35e1fc7dfaae71207d7b62e9e53be314ebd251b53b/sqlmodel-0.0.39-py3-none-any.whl", hash = "sha256:90ebe92ce5cc11d7fff8dc7cb594790a102333c8fe7c14865254f6fc5c939795", size = 29680, upload-time = "2026-06-25T13:01:37.494Z" },
]
[[package]]
name = "starlette"
version = "1.3.1"