move route code out of server into own blueprints and cleanup the codebase

This commit is contained in:
2026-04-06 03:10:49 +02:00
parent 0ebb04f0a2
commit d89986dba9
9 changed files with 477 additions and 373 deletions
+3
View File
@@ -0,0 +1,3 @@
from .battlesnake import create_battlesnake_blueprint
from .metrics import create_metrics_blueprint
from .dashboard import create_dashboard_blueprint
+83
View File
@@ -0,0 +1,83 @@
from typing import TYPE_CHECKING, cast
import json, time, os
from quart import Blueprint, request, jsonify
from quart_common.web.logger import await_log
from server.storage import StorageLoader
from server.GameBoard import GameBoard
if TYPE_CHECKING:
from server.Server import Server
def create_battlesnake_blueprint(server:'Server') -> Blueprint:
blueprint = Blueprint('battlesnake', __name__)
@blueprint.get('/')
async def on_info():
server.metrics_collector.record_http_request('info')
snake_config = await server._read_json_config_or_create()
await await_log(server.logger.info(f'INFO Snake: {snake_config}'))
return snake_config
@blueprint.post('/start')
async def on_start():
server.metrics_collector.record_http_request('start')
await server._prune_stale_games()
game_state = await request.get_json()
await server._create_game_board(game_state)
await server._record_gameplay_start(game_state)
await await_log(server.logger.info(f'GAME START: {game_state['game']}'))
return 'ok'
@blueprint.post('/move')
async def on_move():
server.metrics_collector.record_http_request('move')
game_state = await request.get_json()
move_started = time.perf_counter()
game_board = cast(GameBoard, await server._get_game_board(game_state))
next_move = game_board.snake_neat_make_a_move()
await server._persist_game_board(game_state['game']['id'], game_board)
await server._record_gameplay_turn(game_state, next_move, game_board)
elapsed_ms = (time.perf_counter() - move_started) * 1000.0
await server.metrics_collector.record_move(next_move, elapsed_ms)
if server.debug:
await await_log(server.logger.debug(f'TURN: {game_state['turn']:3}, MOVE: {next_move:5}'))
return {'move': next_move}
@blueprint.post('/end')
async def on_end():
server.metrics_collector.record_http_request('end')
await server._prune_stale_games()
game_state = await request.get_json()
if server.store_game_state:
game_board = cast(GameBoard, await server._get_game_board(game_state, end=True))
if server.check_tls_security:
await game_board.save(
StorageLoader.build(server.storage_type),
file_path=os.path.join(server.data_path, 'data'),
database=os.getenv('EDGEDB_DATABASE', None),
tls_security=None,
)
else:
await game_board.save(
StorageLoader.build(server.storage_type),
file_path=os.path.join(server.data_path, 'data'),
database=os.getenv('EDGEDB_DATABASE', None),
)
await server._record_gameplay_end(game_state)
await server._push_dashboard_games_update(game_state)
await await_log(server.logger.info(f'GAME ENDED: Winner is {[x['name'] for x in game_state['board']['snakes']]}'))
await server._delete_game_board(game_state)
await server.metrics_collector.record_game_end(game_state)
return 'ok'
@blueprint.get("/cleanup")
async def cleanup():
results = server._cleanup_database()
return jsonify(data=json.loads(results), status=200)
return blueprint
+119
View File
@@ -0,0 +1,119 @@
from typing import TYPE_CHECKING
import asyncio, json, os
from quart import (
Blueprint,
render_template,
send_from_directory,
request,
websocket,
)
if TYPE_CHECKING:
from server.Server import Server
def create_dashboard_blueprint(server:'Server') -> Blueprint:
blueprint = Blueprint('dashboard', __name__)
@blueprint.get('/dashboard')
async def dashboard_view():
initial_game_id = request.args.get('game_id', '')
initial_summary = await server._get_dashboard_summary()
initial_games = await server._get_dashboard_games(limit=100)
return await render_template(
'dashboard.html',
initial_game_id=initial_game_id,
initial_summary=initial_summary,
initial_games=initial_games,
)
@blueprint.get('/dashboard/customizations/<path:asset_path>')
async def dashboard_customizations_asset(asset_path:str):
customization_root = os.path.join(
server.data_path,
'server',
'static',
'customizations',
)
return await send_from_directory(customization_root, asset_path)
@blueprint.websocket('/dashboard/ws/games')
async def dashboard_games_ws():
ws_hub = server.dashboard_ws_hub
websocket_task = asyncio.current_task()
if websocket_task is not None:
await server._register_dashboard_ws_task(websocket_task)
subscriber_queue:asyncio.Queue[str] = asyncio.Queue(maxsize=20)
await server._register_dashboard_game_subscriber(subscriber_queue)
try:
initial_payload = await server._build_dashboard_games_event()
await asyncio.wait_for(
websocket.send(json.dumps(initial_payload)), timeout=1.5
)
while True:
queue_task = asyncio.create_task(subscriber_queue.get())
receive_task = asyncio.create_task(websocket.receive())
try:
done, _ = await asyncio.wait(
{queue_task, receive_task},
timeout=1.0,
return_when=asyncio.FIRST_COMPLETED,
)
if len(done) == 0:
if ws_hub.shutdown_event.is_set():
await asyncio.wait_for(
websocket.send(ws_hub.shutdown_message),
timeout=1.5,
)
break
continue
if receive_task in done:
try:
request_payload_raw = receive_task.result()
except Exception:
break
response_event = await server._handle_dashboard_ws_request(request_payload_raw)
if response_event is not None:
await asyncio.wait_for(
websocket.send(json.dumps(response_event)),
timeout=1.5,
)
if queue_task in done:
event_payload = queue_task.result()
if event_payload == ws_hub.shutdown_message:
await asyncio.wait_for(
websocket.send(event_payload), timeout=1.5
)
break
await asyncio.wait_for(
websocket.send(event_payload), timeout=1.5
)
except asyncio.TimeoutError:
if ws_hub.shutdown_event.is_set():
await asyncio.wait_for(
websocket.send(ws_hub.shutdown_message),
timeout=1.5,
)
break
finally:
for pending_task in (queue_task, receive_task):
if not pending_task.done():
pending_task.cancel()
await asyncio.gather(
queue_task, receive_task, return_exceptions=True
)
except asyncio.CancelledError:
pass
except Exception:
pass
finally:
await server._unregister_dashboard_game_subscriber(subscriber_queue)
if websocket_task is not None:
await server._unregister_dashboard_ws_task(websocket_task)
return blueprint
+30
View File
@@ -0,0 +1,30 @@
from quart import Blueprint, jsonify
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from server.Server import Server
def create_metrics_blueprint(server:"Server") -> Blueprint:
blueprint = Blueprint("metrics", __name__)
@blueprint.get("/metrics")
async def metrics():
snapshot = await server.metrics_collector.build_snapshot(
server.game_last_seen_unix,
server.game_move_counts,
)
return jsonify(snapshot)
@blueprint.get("/metrics/prometheus")
async def metrics_prometheus():
snapshot = await server.metrics_collector.build_snapshot(
server.game_last_seen_unix,
server.game_move_counts,
)
return (
server.metrics_collector.build_prometheus_metrics(snapshot),
200,
{"Content-Type": "text/plain; version=0.0.4; charset=utf-8"},
)
return blueprint