Files
snake-python/scripts/cleanup_postgresql_gameplay.py
T
daniel156161 5efab77248 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.
2026-11-03 10:02:40 +01:00

82 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Delete old low-quality replay payloads while preserving game results."""
import argparse
import asyncio
from datetime import datetime, timedelta, timezone
async def cleanup(dsn: str, older_than_days: int, dry_run: bool, vacuum: bool) -> None:
try:
import asyncpg
except ImportError as exc:
raise RuntimeError("asyncpg is required for PostgreSQL cleanup") from exc
cutoff = datetime.now(timezone.utc) - timedelta(days=max(1, older_than_days))
connection = await asyncpg.connect(dsn=dsn)
try:
candidates = int(await connection.fetchval("""
SELECT count(*)
FROM games
WHERE has_replay
AND quality_status = 'low_quality'
AND COALESCE(ended_at, started_at) < $1
""", cutoff))
rows = await connection.fetchrow("""
SELECT
(SELECT count(*) FROM turns t JOIN games g USING (game_id)
WHERE g.has_replay AND g.quality_status = 'low_quality'
AND COALESCE(g.ended_at, g.started_at) < $1) AS turns,
(SELECT count(*) FROM snake_turns t JOIN games g USING (game_id)
WHERE g.has_replay AND g.quality_status = 'low_quality'
AND COALESCE(g.ended_at, g.started_at) < $1) AS snake_turns,
(SELECT count(*) FROM game_snakes t JOIN games g USING (game_id)
WHERE g.has_replay AND g.quality_status = 'low_quality'
AND COALESCE(g.ended_at, g.started_at) < $1) AS game_snakes
""", cutoff)
print(
f"candidates before {cutoff.isoformat()}: games={candidates:,}, "
f"turns={rows['turns']:,}, snake_turns={rows['snake_turns']:,}, "
f"game_snakes={rows['game_snakes']:,}"
)
if dry_run or candidates == 0:
print("dry run: no rows changed" if dry_run else "nothing to clean")
return
async with connection.transaction():
game_ids = await connection.fetch("""
SELECT game_id FROM games
WHERE has_replay
AND quality_status = 'low_quality'
AND COALESCE(ended_at, started_at) < $1
FOR UPDATE
""", cutoff)
ids = [row["game_id"] for row in game_ids]
await connection.execute("DELETE FROM snake_turns WHERE game_id = ANY($1::text[])", ids)
await connection.execute("DELETE FROM turns WHERE game_id = ANY($1::text[])", ids)
await connection.execute("DELETE FROM game_snakes WHERE game_id = ANY($1::text[])", ids)
await connection.execute("""
UPDATE games
SET has_replay = FALSE, quality_status = 'low_quality'
WHERE game_id = ANY($1::text[])
""", ids)
print(f"cleaned replay payloads for {len(ids):,} games; result rows preserved")
if vacuum:
await connection.execute("VACUUM (ANALYZE) games")
await connection.execute("VACUUM (ANALYZE) game_snakes")
await connection.execute("VACUUM (ANALYZE) turns")
await connection.execute("VACUUM (ANALYZE) snake_turns")
print("vacuum/analyze complete")
finally:
await connection.close()
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dsn", required=True)
parser.add_argument("--older-than-days", type=int, default=30)
parser.add_argument("--execute", action="store_true", help="Apply deletion; default is dry-run")
parser.add_argument("--vacuum", action="store_true")
args = parser.parse_args()
asyncio.run(cleanup(args.dsn, args.older_than_days, not args.execute, args.vacuum))
if __name__ == "__main__":
main()