8639e4725f7337e5f5f434e119941ffed310a60d — Jess Porter a month ago 85e6639
implement eval command, to execute sql and output tabulated data (#58)

3 files changed, 47 insertions(+), 3 deletions(-)

M beryllia/__init__.py
M beryllia/database/__init__.py
M requirements.txt
M beryllia/__init__.py => beryllia/__init__.py +22 -1
@@ 3,6 3,7 @@ from datetime import datetime, timedelta
from json import loads as json_loads
from re import compile as re_compile
from shlex import split as shlex_split
from tabulate import tabulate
from typing import Dict, List, Optional, Sequence, Tuple

from irctokens import build, hostmask as hostmask_parse, Hostmask, Line

@@ 13,7 14,7 @@ from ircstates.numerics import RPL_ENDOFMOTD, ERR_NOMOTD, RPL_WELCOME, RPL_YOURE
from ircrobots.ircv3 import Capability

from .config import Config
from .database import Database
from .database import Database, DatabaseError
from .database.common import NickUserHost
from .database.kline import DBKLine
from .normalise import RFC1459SearchNormaliser

@@ 39,6 40,7 @@ class Caller:

PREFERENCES: Dict[str, type] = {"statsp": bool, "knag": bool}

class Server(BaseServer):

@@ 465,6 467,25 @@ class Server(BaseServer):
        await db.preference.set(caller.oper, key, value)
        return [f"set {key} to {value}"]

    async def cmd_eval(self, caller: Caller, args: Sequence[str]) -> Sequence[str]:
            # list() because readonly_eval returns an immutable sequence
            outs_eval = list(await self.database.readonly_eval(args[0]))
        except DatabaseError as e:
            return [f"error: {str(e)}"]

        if not outs_eval:
            return ["no results"]

        headers = outs_eval.pop(0)
        outs = tabulate(outs_eval[:EVAL_MAX], headers=headers).split("\n")

        if len(outs_eval) > EVAL_MAX:
            overflow = len(outs_eval) - EVAL_MAX
            outs.append(f"(and {overflow} more)")

        return outs

    def line_preread(self, line: Line):
        print(f"< {line.format()}")

M beryllia/database/__init__.py => beryllia/database/__init__.py +24 -1
@@ 1,5 1,5 @@
import asyncpg
from typing import Optional
from typing import Any, Optional, Sequence, Tuple

from .cliconn import CliconnTable, CliexitTable
from .nick_change import NickChangeTable

@@ 19,8 19,13 @@ from .freeze_tag import FreezeTagTable
from ..normalise import SearchNormaliser

class DatabaseError(Exception):

class Database(object):
    def __init__(self, pool: asyncpg.Pool, normaliser: SearchNormaliser):
        self._pool = pool

        self.kline = KLineTable(pool, normaliser)
        self.kline_reject = KLineRejectTable(pool, normaliser)

@@ 50,3 55,21 @@ class Database(object):
            user=username, password=password, host=hostname, database=db_name
        return Database(pool, normaliser)

    async def readonly_eval(self, query: str) -> Sequence[Tuple[Any, ...]]:
        async with self._pool.acquire() as conn, conn.transaction(readonly=True):
                rows = await conn.fetch(query)
            except asyncpg.PostgresError as e:
                raise DatabaseError(str(e))

        if not rows:
            return []

        headers = tuple(key for key in rows[0].keys())
        outs = [headers]

        for row in rows:
            outs.append(tuple(value for value in row.values()))

        return outs

M requirements.txt => requirements.txt +1 -1
@@ 3,4 3,4 @@ asyncpg      ~=0.24.0
ircchallenge ~=0.1.2
ircrobots    ~=0.6.1
pyyaml       ~=5.4.1

tabulate     ~=0.8.10