diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 90c98477..4d96b589 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -46,6 +46,7 @@ notebooks = [ [project.scripts] transcribee-migrate = "transcribee_backend.db.run_migrations:main" +transcribee-admin = "transcribee_backend.admin_cli:main" [tool.uv] override-dependencies = [ @@ -69,8 +70,8 @@ generate_openapi = "python -m scripts.generate_openapi" test = "pytest tests/" pyright = "pyright transcribee_backend/" -[tool.setuptools] -packages = ["transcribee_backend", "transcribee_backend.db"] +[tool.setuptools.packages.find] +include = ["transcribee_backend*"] [build-system] requires = ["setuptools", "setuptools-scm"] diff --git a/backend/scripts/create_api_token.py b/backend/scripts/create_api_token.py deleted file mode 100755 index 2f02be92..00000000 --- a/backend/scripts/create_api_token.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python - -import argparse - -from transcribee_backend.auth import create_api_token -from transcribee_backend.db import SessionContextManager - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--name", required=True) - args = parser.parse_args() - with SessionContextManager(path="management_command:create_api_token") as session: - token = create_api_token(session=session, name=args.name) - print(f"Token created: {token.token}") diff --git a/backend/scripts/create_user.py b/backend/scripts/create_user.py deleted file mode 100755 index c8566ca7..00000000 --- a/backend/scripts/create_user.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python - -import argparse -import random -import string - -from transcribee_backend.auth import create_user -from transcribee_backend.db import SessionContextManager -from transcribee_backend.exceptions import UserAlreadyExists - - -def random_password(): - chars = string.ascii_lowercase + string.digits - return "".join(random.choice(chars) for _ in range(20)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--user", required=True) - parser.add_argument("--password", required=False) - args = parser.parse_args() - - password = args.password - - if not password: - password = random_password() - - print("Auto-generated password.") - print("Infos to send to user:") - print() - print(f"Username: {args.user}") - print(f"Password: {password} (Please change)") - print() - - with SessionContextManager(path="management_command:create_user") as session: - try: - user = create_user(session=session, username=args.user, password=password) - print("User created") - except UserAlreadyExists: - print("Could not create user. A user with that name already exists.") diff --git a/backend/scripts/create_user_token.py b/backend/scripts/create_user_token.py deleted file mode 100755 index a1cd55b5..00000000 --- a/backend/scripts/create_user_token.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python - -import argparse -import datetime - -from sqlmodel import select -from transcribee_backend.auth import generate_user_token -from transcribee_backend.db import SessionContextManager -from transcribee_backend.helpers.time import now_tz_aware -from transcribee_backend.models.user import User - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--username", required=True) - parser.add_argument("--valid-days", required=True) - args = parser.parse_args() - with SessionContextManager(path="management_command:create_user_token") as session: - valid_days = int(args.valid_days) - if valid_days < 0: - print("Valid days must be positive") - exit(1) - - valid_until = now_tz_aware() + datetime.timedelta(days=valid_days) - - user = session.exec( - select(User).where(User.username == args.username) - ).one_or_none() - - if user is None: - print(f"User {args.user} not found") - exit(1) - - key, user_token = generate_user_token(user, valid_until=valid_until) - session.add(user_token) - session.commit() - - print(f"User token created and valid until {valid_until}") - print(f"Secret: {key}") diff --git a/backend/scripts/create_worker.py b/backend/scripts/create_worker.py deleted file mode 100755 index 9a2dc3e0..00000000 --- a/backend/scripts/create_worker.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - -import argparse - -from sqlmodel import select -from transcribee_backend import utils -from transcribee_backend.db import SessionContextManager -from transcribee_backend.models import Worker - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--name", required=True) - parser.add_argument("--token", required=False) - args = parser.parse_args() - if args.token is None: - args.token = utils.get_random_string() - - with SessionContextManager(path="management_command:create_worker") as session: - statement = select(Worker).where(Worker.token == args.token) - results = session.exec(statement) - existing_worker = results.one_or_none() - if existing_worker is None: - worker = Worker(name=args.name, token=args.token, last_seen=None) - session.add(worker) - session.commit() - print(f"Worker with token {args.token} created") - else: - print(f"Worker with token {args.token} already exists") diff --git a/backend/scripts/reset_task.py b/backend/scripts/reset_task.py deleted file mode 100755 index b8120ea2..00000000 --- a/backend/scripts/reset_task.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python - -import argparse -import uuid - -from sqlmodel import or_, update -from transcribee_backend.config import settings -from transcribee_backend.db import SessionContextManager -from transcribee_backend.models.task import Task, TaskState - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--uuid", required=True, type=uuid.UUID, help="Task UUID or Document UUID" - ) - parser.add_argument( - "--state", - type=TaskState, - choices=list(TaskState), - default=TaskState.FAILED, - help="State of tasks to reset", - ) - args = parser.parse_args() - with SessionContextManager(path="management_command:reset_task") as session: - task = session.execute( - update(Task) - .where( - or_(Task.id == args.uuid, Task.document_id == args.uuid), - Task.state == args.state, - ) - .values(state=TaskState.NEW, remaining_attempts=settings.task_attempt_limit) - ) - session.commit() diff --git a/backend/scripts/set_password.py b/backend/scripts/set_password.py deleted file mode 100755 index 5e64ebd3..00000000 --- a/backend/scripts/set_password.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python - -import argparse - -from transcribee_backend.auth import change_user_password -from transcribee_backend.db import SessionContextManager -from transcribee_backend.exceptions import UserDoesNotExist - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--user", required=True) - parser.add_argument("--pass", required=True) - args = parser.parse_args() - - with SessionContextManager(path="management_command:set_password") as session: - try: - user = change_user_password( - session=session, username=args.user, new_password=getattr(args, "pass") - ) - print("Password changed") - except UserDoesNotExist: - print("User does not exists") diff --git a/backend/transcribee_backend/admin_cli/__init__.py b/backend/transcribee_backend/admin_cli/__init__.py new file mode 100755 index 00000000..07122fc6 --- /dev/null +++ b/backend/transcribee_backend/admin_cli/__init__.py @@ -0,0 +1,40 @@ +import argparse + +from .command import Command +from .commands.create_api_token import CreateApiTokenCmd +from .commands.create_user import CreateUserCmd +from .commands.create_user_token import CreateUserTokenCmd +from .commands.create_worker import CreateWorkerCmd +from .commands.reset_task import ResetTaskCmd +from .commands.set_password import SetPasswordCmd + +parser = argparse.ArgumentParser() +subparsers = parser.add_subparsers(metavar="COMMAND") + + +def add_command(name: str, description: str, command: Command): + subparser = subparsers.add_parser(name, help=description, description=description) + command.configure_parser(subparser) + subparser.set_defaults(func=command.run) + + +# all commands belong here +add_command("create_api_token", "Create an API token", CreateApiTokenCmd()) +add_command("create_user_token", "Create an user token", CreateUserTokenCmd()) +add_command("create_user", "Create a new user", CreateUserCmd()) +add_command("create_worker", "Register a new worker", CreateWorkerCmd()) +add_command("reset_task", "Reset a task", ResetTaskCmd()) +add_command("set_password", "Set the password of a user", SetPasswordCmd()) + + +def main(): + args = parser.parse_args() + + if "func" in args: + args.func(args) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/backend/transcribee_backend/admin_cli/command.py b/backend/transcribee_backend/admin_cli/command.py new file mode 100644 index 00000000..31c1baed --- /dev/null +++ b/backend/transcribee_backend/admin_cli/command.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from argparse import ArgumentParser + + +class Command(ABC): + def __init__(self): + pass + + @abstractmethod + def configure_parser(self, parser: ArgumentParser): + pass + + @abstractmethod + def run(self, args): + pass diff --git a/backend/transcribee_backend/admin_cli/commands/create_api_token.py b/backend/transcribee_backend/admin_cli/commands/create_api_token.py new file mode 100755 index 00000000..4ae11fe9 --- /dev/null +++ b/backend/transcribee_backend/admin_cli/commands/create_api_token.py @@ -0,0 +1,15 @@ +from transcribee_backend.admin_cli.command import Command +from transcribee_backend.auth import create_api_token +from transcribee_backend.db import SessionContextManager + + +class CreateApiTokenCmd(Command): + def configure_parser(self, parser): + parser.add_argument("--name", required=True) + + def run(self, args): + with SessionContextManager( + path="management_command:create_api_token" + ) as session: + token = create_api_token(session=session, name=args.name) + print(f"Token created: {token.token}") diff --git a/backend/transcribee_backend/admin_cli/commands/create_user.py b/backend/transcribee_backend/admin_cli/commands/create_user.py new file mode 100755 index 00000000..46c8c22a --- /dev/null +++ b/backend/transcribee_backend/admin_cli/commands/create_user.py @@ -0,0 +1,38 @@ +import random +import string + +from transcribee_backend.admin_cli.command import Command +from transcribee_backend.auth import create_user +from transcribee_backend.db import SessionContextManager +from transcribee_backend.exceptions import UserAlreadyExists + + +def random_password(): + chars = string.ascii_lowercase + string.digits + return "".join(random.choice(chars) for _ in range(20)) + + +class CreateUserCmd(Command): + def configure_parser(self, parser): + parser.add_argument("--user", required=True) + parser.add_argument("--password", required=False) + + def run(self, args): + password = args.password + + if not password: + password = random_password() + + print("Auto-generated password.") + print("Infos to send to user:") + print() + print(f"Username: {args.user}") + print(f"Password: {password} (Please change)") + print() + + with SessionContextManager(path="management_command:create_user") as session: + try: + create_user(session=session, username=args.user, password=password) + print("User created") + except UserAlreadyExists: + print("Could not create user. A user with that name already exists.") diff --git a/backend/transcribee_backend/admin_cli/commands/create_user_token.py b/backend/transcribee_backend/admin_cli/commands/create_user_token.py new file mode 100755 index 00000000..ad55c8cc --- /dev/null +++ b/backend/transcribee_backend/admin_cli/commands/create_user_token.py @@ -0,0 +1,40 @@ +import datetime + +from sqlmodel import select +from transcribee_backend.admin_cli.command import Command +from transcribee_backend.auth import generate_user_token +from transcribee_backend.db import SessionContextManager +from transcribee_backend.helpers.time import now_tz_aware +from transcribee_backend.models.user import User + + +class CreateUserTokenCmd(Command): + def configure_parser(self, parser): + parser.add_argument("--username", required=True) + parser.add_argument("--valid-days", required=True) + + def run(self, args): + with SessionContextManager( + path="management_command:create_user_token" + ) as session: + valid_days = int(args.valid_days) + if valid_days < 0: + print("Valid days must be positive") + exit(1) + + valid_until = now_tz_aware() + datetime.timedelta(days=valid_days) + + user = session.exec( + select(User).where(User.username == args.username) + ).one_or_none() + + if user is None: + print(f"User {args.user} not found") + exit(1) + + key, user_token = generate_user_token(user, valid_until=valid_until) + session.add(user_token) + session.commit() + + print(f"User token created and valid until {valid_until}") + print(f"Secret: {key}") diff --git a/backend/transcribee_backend/admin_cli/commands/create_worker.py b/backend/transcribee_backend/admin_cli/commands/create_worker.py new file mode 100755 index 00000000..f57664a8 --- /dev/null +++ b/backend/transcribee_backend/admin_cli/commands/create_worker.py @@ -0,0 +1,32 @@ +from sqlmodel import select +from transcribee_backend import utils +from transcribee_backend.admin_cli.command import Command +from transcribee_backend.db import SessionContextManager +from transcribee_backend.models import Worker + + +class CreateWorkerCmd(Command): + def configure_parser(self, parser): + parser.add_argument("--name", required=True) + parser.add_argument("--token", required=False) + + def run(self, args): + if args.token is None: + args.token = utils.get_random_string() + + with SessionContextManager(path="management_command:create_worker") as session: + statement = select(Worker).where(Worker.token == args.token) + results = session.exec(statement) + existing_worker = results.one_or_none() + if existing_worker is None: + worker = Worker( + name=args.name, + token=args.token, + last_seen=None, + deactivated_at=None, + ) + session.add(worker) + session.commit() + print(f"Worker with token {args.token} created") + else: + print(f"Worker with token {args.token} already exists") diff --git a/backend/transcribee_backend/admin_cli/commands/reset_task.py b/backend/transcribee_backend/admin_cli/commands/reset_task.py new file mode 100755 index 00000000..0f8dc0eb --- /dev/null +++ b/backend/transcribee_backend/admin_cli/commands/reset_task.py @@ -0,0 +1,30 @@ +from sqlmodel import or_, update +from transcribee_backend.admin_cli.command import Command +from transcribee_backend.config import settings +from transcribee_backend.db import SessionContextManager +from transcribee_backend.models.task import Task, TaskState + + +class ResetTaskCmd(Command): + def configure_parser(self, parser): + parser.add_argument("--uuid", required=True) + parser.add_argument( + "--state", + type=TaskState, + choices=list(TaskState), + default=TaskState.FAILED, + ) + + def run(self, args): + with SessionContextManager(path="management_command:reset_task") as session: + session.execute( + update(Task) + .where( + or_(Task.id == args.uuid, Task.document_id == args.uuid), + Task.state == args.state, + ) + .values( + state=TaskState.NEW, remaining_attempts=settings.task_attempt_limit + ) + ) + session.commit() diff --git a/backend/transcribee_backend/admin_cli/commands/set_password.py b/backend/transcribee_backend/admin_cli/commands/set_password.py new file mode 100755 index 00000000..9885527c --- /dev/null +++ b/backend/transcribee_backend/admin_cli/commands/set_password.py @@ -0,0 +1,22 @@ +from transcribee_backend.admin_cli.command import Command +from transcribee_backend.auth import change_user_password +from transcribee_backend.db import SessionContextManager +from transcribee_backend.exceptions import UserDoesNotExist + + +class SetPasswordCmd(Command): + def configure_parser(self, parser): + parser.add_argument("--user", required=True) + parser.add_argument("--pass", required=True) + + def run(self, args): + with SessionContextManager(path="management_command:set_password") as session: + try: + change_user_password( + session=session, + username=args.user, + new_password=getattr(args, "pass"), + ) + print("Password changed") + except UserDoesNotExist: + print("User does not exists") diff --git a/backend/transcribee_backend/admin_cli/main.py b/backend/transcribee_backend/admin_cli/main.py new file mode 100755 index 00000000..bd9bf0cf --- /dev/null +++ b/backend/transcribee_backend/admin_cli/main.py @@ -0,0 +1,36 @@ +import argparse + +from .command import Command +from .commands.create_api_token import CreateApiTokenCmd +from .commands.create_user import CreateUserCmd +from .commands.create_user_token import CreateUserTokenCmd +from .commands.create_worker import CreateWorkerCmd +from .commands.reset_task import ResetTaskCmd +from .commands.set_password import SetPasswordCmd + +parser = argparse.ArgumentParser() +subparsers = parser.add_subparsers(required=True, title="commands") + + +def add_command(name: str, description: str, command: Command): + subparser = subparsers.add_parser(name, help=description, description=description) + command.configure_parser(subparser) + subparser.set_defaults(func=command.run) + + +# all commands belong here +add_command("create_api_token", "Create an API token", CreateApiTokenCmd()) +add_command("create_user_token", "Create an user token", CreateUserTokenCmd()) +add_command("create_user", "Create a new user", CreateUserCmd()) +add_command("create_worker", "Register a new worker", CreateWorkerCmd()) +add_command("reset_task", "Reset a task", ResetTaskCmd()) +add_command("set_password", "Set the password of a user", SetPasswordCmd()) + + +def main(): + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main()