Files
oxspeak_server/scripts/generate_messages.py
T
2026-08-08 14:57:04 +02:00

224 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""Generate test messages directly in the project's SQLite database."""
# python3 scripts/generate_messages.py \
# --db oxspeak.db \
# --channel-id 672e7757-b7df-401b-8e47-8c62e1fb9d7d \
# --user-id d327a80b-83d4-4a53-9c0b-140f60cc0caa \
# --count 1000 \
# --min-words 10 \
# --max-words 500
from __future__ import annotations
import argparse
import random
import sqlite3
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
WORD_POOL = (
"message", "canal", "serveur", "utilisateur", "test", "donnee", "histoire",
"discussion", "contenu", "generation", "curseur", "fenetre", "lecture",
"chargement", "conversation", "exemple", "texte", "systeme", "application",
"client", "serveur", "base", "requete", "resultat", "information", "session",
"connexion", "fonction", "version", "contenu", "rapide", "simple", "aleatoire",
"important", "nouveau", "ancien", "prochain", "precedent", "visible", "local",
"distant", "stable", "chronologique", "variable", "longueur", "performance",
"validation", "operation", "transaction", "historique", "position", "defilement",
)
MESSAGE_MARKER_FORMAT = "[{number:04d}]"
def positive_int(value: str) -> int:
parsed = int(value)
if parsed <= 0:
raise argparse.ArgumentTypeError("must be greater than zero")
return parsed
def parse_uuid(value: str, option_name: str) -> uuid.UUID:
try:
return uuid.UUID(value)
except ValueError as error:
raise argparse.ArgumentTypeError(f"{option_name} is not a valid UUID: {value}") from error
def next_uuid(previous: uuid.UUID | None) -> uuid.UUID:
"""Return a UUID v7 strictly greater than the previous generated ID."""
generated = uuid.uuid7()
if previous is not None and generated.int <= previous.int:
generated = uuid.UUID(int=previous.int + 1)
return generated
def random_message(
rng: random.Random,
min_words: int,
max_words: int,
marker: str,
) -> str:
# The marker itself counts as one word in the requested range.
body_count = rng.randint(max(0, min_words - 1), max_words - 1)
body = " ".join(rng.choices(WORD_POOL, k=body_count))
return f"{marker} {body}".rstrip()
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--db",
type=Path,
default=Path("oxspeak.db"),
help="SQLite database path (default: oxspeak.db)",
)
parser.add_argument("--channel-id", required=True, help="target channel UUID")
parser.add_argument("--user-id", required=True, help="author user UUID")
parser.add_argument(
"--count",
required=True,
type=positive_int,
help="number of messages to insert",
)
parser.add_argument(
"--min-words",
type=positive_int,
default=10,
help="minimum number of words per message (default: 10)",
)
parser.add_argument(
"--max-words",
type=positive_int,
default=500,
help="maximum number of words per message (default: 500)",
)
parser.add_argument(
"--seed",
type=int,
default=None,
help="optional seed to reproduce generated contents",
)
parser.add_argument(
"--batch-size",
type=positive_int,
default=500,
help="number of rows inserted per batch (default: 500)",
)
return parser
def ensure_target_exists(
connection: sqlite3.Connection,
table: str,
identifier: bytes,
label: str,
) -> None:
row = connection.execute(
f'SELECT 1 FROM "{table}" WHERE id = ? LIMIT 1',
(identifier,),
).fetchone()
if row is None:
raise ValueError(f"{label} does not exist in the database")
def generate_messages(
database: Path,
channel_id: uuid.UUID,
user_id: uuid.UUID,
count: int,
batch_size: int,
min_words: int,
max_words: int,
seed: int | None,
) -> None:
started_at = time.monotonic()
connection = sqlite3.connect(database)
connection.execute("PRAGMA foreign_keys = ON")
connection.execute("PRAGMA busy_timeout = 5000")
try:
ensure_target_exists(connection, "channel", channel_id.bytes, "channel")
ensure_target_exists(connection, "user", user_id.bytes, "user")
previous_id: uuid.UUID | None = None
inserted = 0
rng = random.Random(seed)
connection.execute("BEGIN")
try:
while inserted < count:
current_batch_size = min(batch_size, count - inserted)
rows = []
for offset in range(current_batch_size):
message_id = next_uuid(previous_id)
previous_id = message_id
message_number = inserted + offset + 1
marker = MESSAGE_MARKER_FORMAT.format(number=message_number)
content = random_message(rng, min_words, max_words, marker)
created_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
rows.append(
(
message_id.bytes,
channel_id.bytes,
user_id.bytes,
content,
created_at,
None,
None,
)
)
connection.executemany(
"""
INSERT INTO message
(id, channel_id, user_id, content, created_at, updated_at, reply_to_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
rows,
)
inserted += current_batch_size
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
elapsed = time.monotonic() - started_at
print(f"Inserted {count} messages into {database} in {elapsed:.2f}s")
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if args.max_words < args.min_words:
parser.error("--max-words must be greater than or equal to --min-words")
try:
channel_id = parse_uuid(args.channel_id, "--channel-id")
user_id = parse_uuid(args.user_id, "--user-id")
generate_messages(
database=args.db,
channel_id=channel_id,
user_id=user_id,
count=args.count,
batch_size=args.batch_size,
min_words=args.min_words,
max_words=args.max_words,
seed=args.seed,
)
except (OSError, sqlite3.Error, ValueError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())