feat!: adopt Python 3.15 and support BattlesnakeArena
- Require Python 3.15 in project metadata, runtime pin, and Docker stages. - Use lazy imports for database backends and type-only dependencies. - Defer benchmark dependencies until their code paths are used. - Rely on deferred annotations and use Path.info for file discovery. - Accept BattlesnakeArena alongside BattlesnakeEngine on game endpoints. - Advance quart_common for Python 3.15 and multi-agent validation. - Add pytest development dependencies and refresh the uv lockfile. - Test lazy backend loading, import order, and fresh file discovery. - Document the runtime requirements and locked uv workflow. BREAKING CHANGE: Python 3.15 or newer is required; earlier runtimes cannot parse the lazy import syntax.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import cast
|
||||
import asyncio, json, time, os
|
||||
|
||||
from quart import Blueprint, request, jsonify
|
||||
@@ -10,8 +10,7 @@ from snakes import DEFAULT_SNAKE_CONFIG
|
||||
from server.GameBoard import GameBoard
|
||||
from server.Files import read_file
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from server.Server import Server
|
||||
lazy from server.Server import Server
|
||||
|
||||
def create_battlesnake_blueprint(server:'Server') -> Blueprint:
|
||||
blueprint = Blueprint('battlesnake', __name__)
|
||||
@@ -45,7 +44,7 @@ def create_battlesnake_blueprint(server:'Server') -> Blueprint:
|
||||
return snake_json
|
||||
|
||||
@blueprint.post('/start')
|
||||
@require_user_agent("BattlesnakeEngine", abort_code=404)
|
||||
@require_user_agent(("BattlesnakeEngine", "BattlesnakeArena"), abort_code=404)
|
||||
async def on_start():
|
||||
server.metrics_collector.record_http_request('start')
|
||||
await server.game_runtime.prune_stale_games()
|
||||
@@ -59,7 +58,7 @@ def create_battlesnake_blueprint(server:'Server') -> Blueprint:
|
||||
return 'ok'
|
||||
|
||||
@blueprint.post('/move')
|
||||
@require_user_agent("BattlesnakeEngine", abort_code=404)
|
||||
@require_user_agent(("BattlesnakeEngine", "BattlesnakeArena"), abort_code=404)
|
||||
async def on_move():
|
||||
server.metrics_collector.record_http_request('move')
|
||||
game_state = await request.get_json()
|
||||
@@ -91,7 +90,7 @@ def create_battlesnake_blueprint(server:'Server') -> Blueprint:
|
||||
return {'move': next_move}
|
||||
|
||||
@blueprint.post('/end')
|
||||
@require_user_agent("BattlesnakeEngine", abort_code=404)
|
||||
@require_user_agent(("BattlesnakeEngine", "BattlesnakeArena"), abort_code=404)
|
||||
async def on_end():
|
||||
server.metrics_collector.record_http_request('end')
|
||||
await server.game_runtime.prune_stale_games()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import TYPE_CHECKING
|
||||
import asyncio, json, os
|
||||
|
||||
from quart import (
|
||||
@@ -9,8 +8,7 @@ from quart import (
|
||||
websocket,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from server.Server import Server
|
||||
lazy from server.Server import Server
|
||||
|
||||
def create_dashboard_blueprint(server:'Server') -> Blueprint:
|
||||
blueprint = Blueprint('dashboard', __name__)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from quart import Blueprint, jsonify, request
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from server.Server import Server
|
||||
lazy from server.Server import Server
|
||||
|
||||
def create_metrics_blueprint(server:'Server') -> Blueprint:
|
||||
blueprint = Blueprint('metrics', __name__)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from server.GameBoard import GameBoard
|
||||
lazy from server.GameBoard import GameBoard
|
||||
|
||||
from server.dataset.Dataset import Dataset
|
||||
from server.Files import save_file
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from .Template import GameplayBackendTemplate
|
||||
|
||||
lazy from .PostgresqlGameplayBackend import PostgresqlGameplayBackend as PostgresqlBackend
|
||||
lazy from .SqliteGameplayBackend import SqliteGameplayBackend as SqliteBackend
|
||||
|
||||
class GameplayBackendBuilder:
|
||||
@staticmethod
|
||||
def build(backend:str="sqlite", db_path:str|None=None, busy_timeout_ms:int=5000, pg_dsn:str|None=None, pg_min_size:int=1, pg_max_size:int=5) -> GameplayBackendTemplate:
|
||||
normalized = (backend or "sqlite").strip().lower()
|
||||
|
||||
if normalized == "postgresql" or normalized == "postgres":
|
||||
from .PostgresqlGameplayBackend import PostgresqlGameplayBackend
|
||||
if not pg_dsn:
|
||||
raise ValueError("pg_dsn is required for the postgresql backend")
|
||||
return PostgresqlGameplayBackend(
|
||||
return PostgresqlBackend(
|
||||
dsn=pg_dsn,
|
||||
min_size=pg_min_size,
|
||||
max_size=pg_max_size,
|
||||
@@ -17,9 +19,8 @@ class GameplayBackendBuilder:
|
||||
)
|
||||
|
||||
if normalized == "sqlite":
|
||||
from .SqliteGameplayBackend import SqliteGameplayBackend
|
||||
if not db_path:
|
||||
raise ValueError("db_path is required for the sqlite backend")
|
||||
return SqliteGameplayBackend(db_path=db_path, busy_timeout_ms=busy_timeout_ms)
|
||||
return SqliteBackend(db_path=db_path, busy_timeout_ms=busy_timeout_ms)
|
||||
|
||||
raise ValueError(f"Unknown gameplay backend: {backend!r}. Choose 'sqlite' or 'postgresql'.")
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"""Load sampled gameplay positions from SQLite or PostgreSQL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from server.GameBoard import GameBoard
|
||||
lazy from server.GameBoard import GameBoard
|
||||
|
||||
class Dataset:
|
||||
VALID_MOVES = {"up", "down", "left", "right"}
|
||||
|
||||
@@ -16,7 +16,7 @@ class DatasetIO:
|
||||
|
||||
for item in input_files:
|
||||
path = Path(item)
|
||||
if path.is_dir():
|
||||
if path.info.is_dir():
|
||||
for pattern in patterns:
|
||||
for file_path in sorted(path.rglob(pattern)):
|
||||
key = str(file_path.resolve())
|
||||
@@ -29,7 +29,7 @@ class DatasetIO:
|
||||
if any(ch in item for ch in "*?[]"):
|
||||
for match in sorted(glob.glob(item)):
|
||||
file_path = Path(match)
|
||||
if not file_path.is_file():
|
||||
if not file_path.info.is_file():
|
||||
continue
|
||||
key = str(file_path.resolve())
|
||||
if key in seen:
|
||||
@@ -38,7 +38,7 @@ class DatasetIO:
|
||||
resolved.append(file_path)
|
||||
continue
|
||||
|
||||
if path.is_file():
|
||||
if path.info.is_file():
|
||||
key = str(path.resolve())
|
||||
if key in seen:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user