- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests. - Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без выпущенных ключей доступа нет. В базе — только sha256 ключа. - Задача и окно сессии привязаны к клиенту: чужой ключ их не видит. - Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу. - python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats. - smoke и loadtest с ключом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9dc2c0b619
commit
1a4bd8d672
34 files changed
+857
-81
No files matched your search
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Клиенты и API-ключи:
|
||||
|
||||
python -m db.cli create <name> # выпустить ключ: печатается один раз
|
||||
python -m db.cli list
|
||||
python -m db.cli revoke <id>
|
||||
python -m db.cli stats [--days 7] # запросы, их время и время работы браузера по клиентам
|
||||
|
||||
В compose — через make: make key-create name=..., make key-list, make key-revoke id=..., make stats days=...
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
from db import clients, stats
|
||||
from db.engine import connect
|
||||
|
||||
|
||||
async def run(args) -> None:
|
||||
db = connect()
|
||||
try:
|
||||
await db.init()
|
||||
if args.command == "create":
|
||||
client, key = await clients.create_client(db, args.name)
|
||||
print(f"client {client.id} ({client.name}), API key — сохраните, больше он не показывается:\n{key}")
|
||||
elif args.command == "list":
|
||||
for c in await clients.list_clients(db):
|
||||
state = f"revoked {c.revoked_at:%Y-%m-%d %H:%M}" if c.revoked_at else "active"
|
||||
last = f"{c.last_access_at:%Y-%m-%d %H:%M}" if c.last_access_at else "never"
|
||||
print(f"{c.id:>4} {c.key_prefix}… {state:<24} last access {last:<16} {c.name}")
|
||||
elif args.command == "revoke":
|
||||
print("revoked" if await clients.revoke(db, args.id) else "no active client with this id")
|
||||
elif args.command == "stats":
|
||||
print_stats(await stats.usage(db, stats.since_days(args.days)), args.days)
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
def _ms(value: int | None) -> str:
|
||||
return "—" if value is None else f"{value / 1000:.1f}s"
|
||||
|
||||
|
||||
def print_stats(rows: list, days: float) -> None:
|
||||
print(f"за {days:g} дн.")
|
||||
print(f"{'клиент':<24} {'запросов':>8} {'ok':>6} {'ср.':>7} {'p95':>7} {'окон':>5} {'браузер, ч':>10} ошибки")
|
||||
for u in rows:
|
||||
errors = ", ".join(f"{k} {v}" for k, v in sorted(u.errors.items())) or "—"
|
||||
name = f"{u.client_id or '-'} {u.name}"[:24]
|
||||
print(
|
||||
f"{name:<24} {u.requests:>8} {u.ok:>6} {_ms(u.avg_ms):>7} {_ms(u.p95_ms):>7} "
|
||||
f"{u.windows:>5} {u.browser_seconds / 3600:>10.2f} {errors}"
|
||||
)
|
||||
if not rows:
|
||||
print("запросов не было")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(prog="python -m db.cli", description="Клиенты Robotex и их API-ключи")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
sub.add_parser("create", help="выпустить ключ новому клиенту").add_argument("name")
|
||||
sub.add_parser("list", help="список клиентов")
|
||||
sub.add_parser("revoke", help="отозвать ключ").add_argument("id", type=int)
|
||||
sub.add_parser("stats", help="статистика использования по клиентам").add_argument("--days", type=float, default=7)
|
||||
asyncio.run(run(parser.parse_args()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user