Compare commits

..

No commits in common. "e097917633260623a7190159ec25a92df9bcba71" and "324788f9a15fd48e9f9a9e191594c5d51b834f31" have entirely different histories.

7 changed files with 464 additions and 56 deletions

View File

@ -3,8 +3,6 @@ import logging
import logging.config import logging.config
import amanuensis.cli.admin import amanuensis.cli.admin
import amanuensis.cli.lexicon
import amanuensis.cli.user
LOGGING_CONFIG = { LOGGING_CONFIG = {
@ -16,7 +14,7 @@ LOGGING_CONFIG = {
}, },
"fmt_detailed": { "fmt_detailed": {
"validate": True, "validate": True,
"format": "%(asctime)s %(levelname)s %(message)s", "format": "%(asctime)s %(levelname)s %(message)s"
}, },
}, },
"handlers": { "handlers": {
@ -29,8 +27,8 @@ LOGGING_CONFIG = {
"loggers": { "loggers": {
__name__: { __name__: {
"level": "DEBUG", "level": "DEBUG",
"handlers": ["hnd_stderr"], "handlers": ["hnd_stderr"]
}, }
}, },
} }
@ -69,7 +67,7 @@ def add_subcommand(subparsers, module) -> None:
def init_logger(args): def init_logger(args):
"""Set up logging based on verbosity args""" """Set up logging based on verbosity args"""
if args.verbose: if (args.verbose):
handler = LOGGING_CONFIG["handlers"]["hnd_stderr"] handler = LOGGING_CONFIG["handlers"]["hnd_stderr"]
handler["formatter"] = "fmt_detailed" handler["formatter"] = "fmt_detailed"
handler["level"] = "DEBUG" handler["level"] = "DEBUG"
@ -89,8 +87,6 @@ def main():
# Add commands from cli submodules # Add commands from cli submodules
subparsers = parser.add_subparsers(metavar="COMMAND") subparsers = parser.add_subparsers(metavar="COMMAND")
add_subcommand(subparsers, amanuensis.cli.admin) add_subcommand(subparsers, amanuensis.cli.admin)
add_subcommand(subparsers, amanuensis.cli.lexicon)
add_subcommand(subparsers, amanuensis.cli.user)
# Parse args and execute the desired action # Parse args and execute the desired action
args = parser.parse_args() args = parser.parse_args()

View File

@ -14,9 +14,7 @@ COMMAND_HELP = "Interact with Amanuensis."
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@add_argument( @add_argument("path", metavar="DB_PATH", help="Path to where the database should be created")
"path", metavar="DB_PATH", help="Path to where the database should be created"
)
@add_argument("--force", "-f", action="store_true", help="Overwrite existing database") @add_argument("--force", "-f", action="store_true", help="Overwrite existing database")
@add_argument("--verbose", "-v", action="store_true", help="Enable db echo") @add_argument("--verbose", "-v", action="store_true", help="Enable db echo")
def command_init_db(args) -> int: def command_init_db(args) -> int:

View File

@ -1,30 +1,324 @@
# Standard library imports
import logging import logging
from .helpers import add_argument # Module imports
from amanuensis.config import RootConfigDirectoryContext
from amanuensis.models import LexiconModel, UserModel
COMMAND_NAME = "lexicon"
COMMAND_HELP = "Interact with lexicons." from .helpers import (
add_argument, no_argument, requires_lexicon, requires_user, alias,
LOG = logging.getLogger(__name__) config_get, config_set, CONFIG_GET_ROOT_VALUE)
logger = logging.getLogger(__name__)
#
# CRUD commands
#
@alias('lc')
@add_argument("--name", required=True, help="The name of the new lexicon")
@requires_user
@add_argument("--prompt", help="The lexicon's prompt")
def command_create(args): def command_create(args):
""" """
Create a lexicon. Create a lexicon
The specified user will be the editor. A newly created created lexicon is
not open for joining and requires additional configuration before it is
playable. The editor should ensure that all settings are as desired before
opening the lexicon for player joins.
""" """
raise NotImplementedError() # Module imports
from amanuensis.lexicon import valid_name, create_lexicon
root: RootConfigDirectoryContext = args.root
# Verify arguments
if not valid_name(args.name):
logger.error(f'Lexicon name contains illegal characters: "{args.name}"')
return -1
with root.lexicon.read_index() as index:
if args.name in index.keys():
logger.error(f'A lexicon with name "{args.name}" already exists')
return -1
# Perform command
create_lexicon(root, args.name, args.user)
# Output already logged by create_lexicon
return 0
@alias('ld')
@requires_lexicon
@add_argument("--purge", action="store_true", help="Delete the lexicon's data")
def command_delete(args): def command_delete(args):
""" """
Delete a lexicon. Delete a lexicon and optionally its data
""" """
raise NotImplementedError() raise NotImplementedError()
# # Module imports
# from amanuensis.config import logger
# from amanuensis.lexicon.manage import delete_lexicon
# # Perform command
# delete_lexicon(args.lexicon, args.purge)
# # Output
# logger.info('Deleted lexicon "{}"'.format(args.lexicon.name))
# return 0
@alias('ll')
@no_argument
def command_list(args): def command_list(args):
""" """
List all lexicons and their statuses. List all lexicons and their statuses
""" """
raise NotImplementedError() raise NotImplementedError()
# # Module imports
# from amanuensis.lexicon.manage import get_all_lexicons
# # Execute command
# lexicons = get_all_lexicons()
# # Output
# statuses = []
# for lex in lexicons:
# statuses.append("{0.lid} {0.name} ({1})".format(lex, lex.status()))
# for s in statuses:
# print(s)
# return 0
@alias('ln')
@requires_lexicon
@add_argument("--get",
metavar="PATHSPEC",
dest="get",
nargs="?",
const=CONFIG_GET_ROOT_VALUE,
help="Get the value of a config key")
@add_argument("--set",
metavar=("PATHSPEC", "VALUE"),
dest="set",
nargs=2,
help="Set the value of a config key")
def command_config(args):
"""
Interact with a lexicon's config
"""
lexicon: LexiconModel = args.lexicon
# Verify arguments
if args.get and args.set:
logger.error("Specify one of --get and --set")
return -1
# Execute command
if args.get:
config_get(lexicon.cfg, args.get)
if args.set:
with lexicon.ctx.edit_config() as cfg:
config_set(lexicon.lid, cfg, args.set)
# config_* functions handle output
return 0
#
# Player/character commands
#
@alias('lpa')
@requires_lexicon
@requires_user
def command_player_add(args):
"""
Add a player to a lexicon
"""
lexicon: LexiconModel = args.lexicon
user: UserModel = args.user
# Module imports
from amanuensis.lexicon import add_player_to_lexicon
# Verify arguments
if user.uid in lexicon.cfg.join.joined:
logger.error(f'"{user.cfg.username}" is already a player '
f'in "{lexicon.cfg.name}"')
return -1
# Perform command
add_player_to_lexicon(user, lexicon)
# Output
logger.info(f'Added user "{user.cfg.username}" to '
f'lexicon "{lexicon.cfg.name}"')
return 0
@alias('lpr')
@requires_lexicon
@requires_user
def command_player_remove(args):
"""
Remove a player from a lexicon
Removing a player dissociates them from any characters
they control but does not delete any character data.
"""
raise NotImplementedError()
# # Module imports
# from amanuensis.lexicon.manage import remove_player
# # Verify arguments
# if not args.user.in_lexicon(args.lexicon):
# logger.error('"{0.username}" is not a player in lexicon "{1.name}"'
# ''.format(args.user, args.lexicon))
# return -1
# if args.user.id == args.lexicon.editor:
# logger.error("Can't remove the editor of a lexicon")
# return -1
# # Perform command
# remove_player(args.lexicon, args.user)
# # Output
# logger.info('Removed "{0.username}" from lexicon "{1.name}"'.format(
# args.user, args.lexicon))
# return 0
@alias('lpl')
@requires_lexicon
def command_player_list(args):
"""
List all players in a lexicon
"""
raise NotImplementedError()
# import json
# # Module imports
# from amanuensis.user import UserModel
# # Perform command
# players = list(map(
# lambda uid: UserModel.by(uid=uid).username,
# args.lexicon.join.joined))
# # Output
# print(json.dumps(players, indent=2))
# return 0
@alias('lcc')
@requires_lexicon
@requires_user
@add_argument("--charname", required=True, help="The character's name")
def command_char_create(args):
"""
Create a character for a lexicon
The specified player will be set as the character's player.
"""
lexicon: LexiconModel = args.lexicon
user: UserModel = args.user
# Module imports
from amanuensis.lexicon import create_character_in_lexicon
# Verify arguments
if user.uid not in lexicon.cfg.join.joined:
logger.error('"{0.username}" is not a player in lexicon "{1.name}"'
''.format(user.cfg, lexicon.cfg))
return -1
# Perform command
create_character_in_lexicon(user, lexicon, args.charname)
# Output
logger.info(f'Created character "{args.charname}" for "{user.cfg.username}"'
f' in "{lexicon.cfg.name}"')
return 0
@alias('lcd')
@requires_lexicon
@add_argument("--charname", required=True, help="The character's name")
def command_char_delete(args):
"""
Delete a character from a lexicon
Deleting a character dissociates them from any content
they have contributed rather than deleting it.
"""
raise NotImplementedError()
# # Module imports
# from amanuensis.lexicon import LexiconModel
# from amanuensis.lexicon.manage import delete_character
# # Verify arguments
# lex = LexiconModel.by(name=args.lexicon)
# if lex is None:
# logger.error("Could not find lexicon '{}'".format(args.lexicon))
# return -1
# # Internal call
# delete_character(lex, args.charname)
# return 0
@alias('lcl')
@requires_lexicon
def command_char_list(args):
"""
List all characters in a lexicon
"""
raise NotImplementedError()
# import json
# # Module imports
# from amanuensis.lexicon import LexiconModel
# # Verify arguments
# lex = LexiconModel.by(name=args.lexicon)
# if lex is None:
# logger.error("Could not find lexicon '{}'".format(args.lexicon))
# return -1
# # Internal call
# print(json.dumps(lex.character, indent=2))
# return 0
#
# Procedural commands
#
@alias('lpt')
@requires_lexicon
@add_argument("--as-deadline",
action="store_true",
help="Notifies players of the publish result")
@add_argument("--force",
action="store_true",
help="Publish all approved articles, regardless of other checks")
def command_publish_turn(args):
"""
Publishes the current turn of a lexicon
The --as-deadline flag is intended to be used only by the scheduled publish
attempts controlled by the publish.deadlines setting.
The --force flag bypasses the publish.quorum and publish.block_on_ready
settings.
"""
# Module imports
from amanuensis.lexicon import attempt_publish
# Internal call
result = attempt_publish(args.lexicon)
if not result:
logger.error('Publish failed, check lexicon log')

View File

@ -1,37 +1,158 @@
# Standard library imports
import getpass
import logging import logging
# import shutil
from .helpers import add_argument # Module imports
from amanuensis.models import UserModel
COMMAND_NAME = "user" from .helpers import (
COMMAND_HELP = "Interact with users." add_argument,
no_argument,
LOG = logging.getLogger(__name__) requires_user,
alias,
config_get,
config_set,
CONFIG_GET_ROOT_VALUE)
logger = logging.getLogger(__name__)
@alias('uc')
@add_argument("--username", required=True, help="Name of user to create")
@add_argument("--email", help="User's email")
@add_argument("--displayname", help="User's publicly displayed name")
def command_create(args): def command_create(args):
""" """
Create a user. Create a user
""" """
raise NotImplementedError() # Module imports
from amanuensis.user import (
valid_username, valid_email, create_user)
# Verify arguments
if not valid_username(args.username):
logger.error("Invalid username: usernames may only contain alphanumer"
"ic characters, dashes, and underscores")
return -1
if not args.displayname:
args.displayname = args.username
if args.email and not valid_email(args.email):
logger.error("Invalid email")
return -1
try:
existing_user = args.model_factory.user(args.username)
if existing_user is not None:
logger.error("Invalid username: username is already taken")
return -1
except Exception:
pass # User doesn't already exist, good to go
# Perform command
new_user, tmp_pw = create_user(
args.root,
args.model_factory,
args.username,
args.displayname,
args.email)
# Output
print(tmp_pw)
return 0
@alias('ud')
@requires_user
def command_delete(args): def command_delete(args):
""" """
Delete a user. Delete a user
""" """
raise NotImplementedError() raise NotImplementedError()
# # Module imports
# from amanuensis.config import logger, prepend, json_rw
# # Perform command
# user_path = prepend('user', args.user.id)
# shutil.rmtree(user_path)
# with json_rw('user', 'index.json') as index:
# del index[args.user.username]
# # TODO resolve user id references in all games
# # Output
# logger.info("Deleted user {0.username} ({0.id})".format(args.user))
# return 0
@alias('ul')
@no_argument
def command_list(args): def command_list(args):
""" """List all users"""
List all users.
"""
raise NotImplementedError() raise NotImplementedError()
# # Module imports
# from amanuensis.config import prepend, json_ro
# from amanuensis.user import UserModel
# # Perform command
# users = []
# with json_ro('user', 'index.json') as index:
# for username, uid in index.items():
# users.append(UserModel.by(uid=uid))
# # Output
# users.sort(key=lambda u: u.username)
# for user in users:
# print("{0.id} {0.displayname} ({0.username})".format(user))
# return 0
@alias('un')
@requires_user
@add_argument(
"--get", metavar="PATHSPEC", dest="get",
nargs="?", const=CONFIG_GET_ROOT_VALUE, help="Get the value of a config key")
@add_argument(
"--set", metavar=("PATHSPEC", "VALUE"), dest="set",
nargs=2, help="Set the value of a config key")
def command_config(args):
"""
Interact with a user's config
"""
user: UserModel = args.user
# Verify arguments
if args.get and args.set:
logger.error("Specify one of --get and --set")
return -1
# Perform command
if args.get:
config_get(user.cfg, args.get)
if args.set:
with user.ctx.edit_config() as cfg:
config_set(user.uid, cfg, args.set)
# Output
return 0
@alias('up')
@requires_user
@add_argument("--password", help="The password to set. Used for scripting; "
"not recommended for general use")
def command_passwd(args): def command_passwd(args):
""" """
Set a user's password. Set a user's password
""" """
raise NotImplementedError() user: UserModel = args.user
# Verify arguments
password: str = args.password or getpass.getpass("Password: ")
# Perform command
user.set_password(password)
# Output
logger.info('Updated password for {}'.format(user.cfg.username))
return 0

View File

@ -29,7 +29,6 @@ class EnvironmentConfig(AmanuensisConfig):
class CommandLineConfig(AmanuensisConfig): class CommandLineConfig(AmanuensisConfig):
"""Loads config values from command line arguments.""" """Loads config values from command line arguments."""
def __init__(self) -> None: def __init__(self) -> None:
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument("--config-file", default=AmanuensisConfig.CONFIG_FILE) parser.add_argument("--config-file", default=AmanuensisConfig.CONFIG_FILE)

View File

@ -1,4 +1,4 @@
[mypy] [mypy]
ignore_missing_imports = true ignore_missing_imports = true
exclude = "|amanuensis/lexicon/.*|amanuensis/models/.*|amanuensis/resources/.*|amanuensis/server/.*|amanuensis/user/.*|amanuensis/__main__.py|" exclude = "amanuensis/cli/.*|amanuensis/config/.*|amanuensis/lexicon/.*|amanuensis/log/.*|amanuensis/models/.*|amanuensis/resources/.*|amanuensis/server/.*|amanuensis/user/.*|amanuensis/__main__.py"
; mypy stable doesn't support pyproject.toml yet ; mypy stable doesn't support pyproject.toml yet

View File

@ -21,7 +21,7 @@ amanuensis-cli = "amanuensis.cli:main"
amanuensis-server = "amanuensis.server:run" amanuensis-server = "amanuensis.server:run"
[tool.black] [tool.black]
extend-exclude = "^/amanuensis/lexicon/.*|^/amanuensis/models/.*|^/amanuensis/resources/.*|^/amanuensis/server/.*|^/amanuensis/user/.*|^/amanuensis/__main__.py" extend-exclude = "^/amanuensis/cli/.*|^/amanuensis/config/.*|^/amanuensis/lexicon/.*|^/amanuensis/log/.*|^/amanuensis/models/.*|^/amanuensis/resources/.*|^/amanuensis/server/.*|^/amanuensis/user/.*|^/amanuensis/__main__.py"
[tool.mypy] [tool.mypy]
ignore_missing_imports = true ignore_missing_imports = true