Compare commits

...

3 Commits

Author SHA1 Message Date
Ferdinand Thiessen 2880076705 [cli] Fix exporting of plugin interfaces
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-07-31 19:07:40 +02:00
Ferdinand Thiessen fa503fe142 [cli] Added install command to install the database and all plugins
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-07-31 19:07:40 +02:00
Ferdinand Thiessen f1d6b6a2f2 [plugins][cli] Fix initial migration file + Make sure plugin permissions are installed
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-07-31 19:07:32 +02:00
13 changed files with 108 additions and 43 deletions

View File

@ -50,10 +50,16 @@ If not you need to create user and database manually do (or similar on Windows):
) | sudo mysql
Then you can install the database tables, this will update all tables from core + all enabled plugins.
*Hint:* The same command can be later used to upgrade the database after plugins or core are updated.
And also install all enabled plugins:
$ flaschengeist install
*Hint:* To only install the database tables, or upgrade the database after plugins or core are updated later
you can use this command:
$ flaschengeist db upgrade heads
## Plugins
To only upgrade one plugin (for example the `events` plugin):
@ -88,6 +94,7 @@ with the difference of the main logger will be forced to output to `stderr` and
of the CLI will override the logging level you have configured for the main logger.
$ flaschengeist run
or with debug messages:
$ flaschengeist run --debug

View File

@ -0,0 +1,4 @@
from pathlib import Path
alembic_migrations = str(Path(__file__).resolve().parent / "migrations")

View File

@ -18,14 +18,21 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"plugin_setting",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("plugin", sa.String(length=127), nullable=True),
sa.Column("name", sa.String(length=127), nullable=False),
sa.Column("value", sa.PickleType(protocol=4), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin_setting")),
)
op.create_table(
"image",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("filename_", sa.String(length=127), nullable=False),
sa.Column("mimetype_", sa.String(length=30), nullable=False),
sa.Column("thumbnail_", sa.String(length=127), nullable=True),
sa.Column("path_", sa.String(length=127), nullable=True),
sa.Column("filename", sa.String(length=255), nullable=False),
sa.Column("mimetype", sa.String(length=127), nullable=False),
sa.Column("thumbnail", sa.String(length=255), nullable=True),
sa.Column("path", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_image")),
)
op.create_table(

View File

@ -88,12 +88,14 @@ def main(*args, **kwargs):
from .export_cmd import export
from .docs_cmd import docs
from .run_cmd import run
from .install_cmd import install
# Override logging level
environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX))
cli.add_command(export)
cli.add_command(docs)
cli.add_command(install)
cli.add_command(plugin)
cli.add_command(run)
cli(*args, **kwargs)

View File

@ -1,4 +1,5 @@
import click
from importlib_metadata import entry_points
@click.command()
@ -8,14 +9,21 @@ import click
@click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True)
def export(namespace, output, no_core, plugin):
from flaschengeist import logger, models
from flaschengeist.app import get_plugins
from .InterfaceGenerator import InterfaceGenerator
gen = InterfaceGenerator(namespace, output, logger)
if not no_core:
gen.run(models)
if plugin:
for plugin_class in get_plugins():
if (len(plugin) == 0 or plugin_class.id in plugin) and plugin_class.models is not None:
gen.run(plugin_class.models)
for entry_point in entry_points(group="flaschengeist.plugins"):
if len(plugin) == 0 or entry_point.name in plugin:
try:
plugin = entry_point.load()
gen.run(plugin.models)
except:
logger.error(
f"Plugin {entry_point.name} could not be loaded due to an error.",
exc_info=True,
)
continue
gen.write()

View File

@ -0,0 +1,20 @@
import click
from click.decorators import pass_context
from flask.cli import with_appcontext
from flask_migrate import upgrade
from flaschengeist.alembic import alembic_migrations
from flaschengeist.cli.plugin_cmd import install_plugin_command
from flaschengeist.utils.hook import Hook
@click.command()
@with_appcontext
@pass_context
@Hook("plugins.installed")
def install(ctx):
# Install database
upgrade(alembic_migrations, revision="heads")
# Install plugins
install_plugin_command(ctx, [], True)

View File

@ -4,7 +4,9 @@ from flask import current_app
from flask.cli import with_appcontext
from importlib_metadata import EntryPoint, entry_points
from flaschengeist.database import db
from flaschengeist.config import config
from flaschengeist.models.user import Permission
@click.group()
@ -12,6 +14,35 @@ def plugin():
pass
def install_plugin_command(ctx, plugin, all):
"""Install one or more plugins"""
if not all and len(plugin) == 0:
ctx.fail("At least one plugin must be specified, or use `--all` flag.")
if all:
plugins = current_app.config["FG_PLUGINS"]
else:
try:
plugins = {plugin_name: current_app.config["FG_PLUGINS"][plugin_name] for plugin_name in plugin}
except KeyError as e:
ctx.fail(f"Invalid plugin name, could not find >{e.args[0]}<")
for name, plugin in plugins.items():
click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False)
# Install permissions
if plugin.permissions:
cur_perm = set(x.name for x in Permission.query.filter(Permission.name.in_(plugin.permissions)).all())
all_perm = set(plugin.permissions)
add = all_perm - cur_perm
if add:
db.session.bulk_save_objects([Permission(name=x) for x in all_perm])
db.session.commit()
# Custom installation steps
plugin.install()
click.secho(" ok", fg="green")
@plugin.command()
@click.argument("plugin", nargs=-1, type=str)
@click.option("--all", help="Install all enabled plugins", is_flag=True)
@ -19,20 +50,7 @@ def plugin():
@pass_context
def install(ctx, plugin, all):
"""Install one or more plugins"""
if not all and len(plugin) == 0:
ctx.fail("At least one plugin must be specified, or use `--all` flag.")
if all:
plugins = current_app.config["FG_PLUGINS"].values()
else:
try:
plugins = [current_app.config["FG_PLUGINS"][p] for p in plugin]
except KeyError as e:
ctx.fail(f"Invalid plugin ID, could not find >{e.args[0]}<")
for p in plugins:
name = p.id.split(".")[-1]
click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False)
p.install()
click.secho(" ok", fg="green")
return install_plugin_command(ctx, plugin, all)
@plugin.command()
@ -44,14 +62,16 @@ def uninstall(ctx: click.Context, plugin):
if len(plugin) == 0:
ctx.fail("At least one plugin must be specified")
try:
plugins = [current_app.config["FG_PLUGINS"][p] for p in plugin]
plugins = {plugin_name: current_app.config["FG_PLUGINS"][plugin_name] for plugin_name in plugin}
except KeyError as e:
ctx.fail(f"Invalid plugin ID, could not find >{e.args[0]}<")
if (
click.prompt(
"You are going to uninstall:\n\n"
f"\t{', '.join([p.id.split('.')[-1] for p in plugins])}\n\n"
f"\t{', '.join([plugin_name for plugin_name in plugins.keys()])}\n\n"
"Are you sure?",
default="n",
show_choices=True,
@ -60,10 +80,9 @@ def uninstall(ctx: click.Context, plugin):
!= "y"
):
ctx.exit()
for p in plugins:
name = p.id.split(".")[-1]
for name, plugin in plugins.items():
click.echo(f"Uninstalling {name}{'.'*(20-len(name))}", nl=False)
p.uninstall()
plugin.uninstall()
click.secho(" ok", fg="green")
@ -78,7 +97,7 @@ def ls(enabled, no_header):
return p.version
plugins = entry_points(group="flaschengeist.plugins")
enabled_plugins = [key for key, value in config.items() if "enabled" in value] + [config["FLASCHENGEIST"]["auth"]]
enabled_plugins = [key for key, value in config.items() if "enabled" in value and value["enabled"]] + [config["FLASCHENGEIST"]["auth"]]
loaded_plugins = current_app.config["FG_PLUGINS"].keys()
if not no_header:

View File

@ -9,11 +9,11 @@ from ..database import db
class Image(db.Model, ModelSerializeMixin):
__tablename__ = "image"
id: int = db.Column("id", Serial, primary_key=True)
filename_: str = db.Column(db.String(127), nullable=False)
mimetype_: str = db.Column(db.String(30), nullable=False)
thumbnail_: str = db.Column(db.String(127))
path_: str = db.Column(db.String(127))
id: int = db.Column(Serial, primary_key=True)
filename_: str = db.Column("filename", db.String(255), nullable=False)
mimetype_: str = db.Column("mimetype", db.String(127), nullable=False)
thumbnail_: str = db.Column("thumbnail", db.String(255))
path_: str = db.Column("path", db.String(255))
def open(self):
return open(self.path_, "rb")

View File

@ -8,6 +8,6 @@ from ..database import db
class _PluginSetting(db.Model):
__tablename__ = "plugin_setting"
id = db.Column("id", Serial, primary_key=True)
plugin: str = db.Column(db.String(30))
name: str = db.Column(db.String(30), nullable=False)
plugin: str = db.Column(db.String(127))
name: str = db.Column(db.String(127), nullable=False)
value: Any = db.Column(db.PickleType(protocol=4))

View File

@ -126,7 +126,8 @@ class Plugin:
def install(self):
"""Installation routine
Is always called with Flask application context
Is always called with Flask application context,
it is called after the plugin permissions are installed.
"""
pass

View File

@ -13,7 +13,6 @@ from flaschengeist.controller import sessionController, userController
class AuthRoutePlugin(Plugin):
id = "dev.flaschengeist.auth"
blueprint = Blueprint("auth", __name__)

View File

@ -14,12 +14,10 @@ from flaschengeist import logger
class AuthPlain(AuthPlugin):
id = "auth_plain"
def install(self):
plugins_installed(self.post_install)
def post_install(self, **kwargs):
def post_install(self, *args, **kwargs):
if User.query.filter(User.deleted == False).count() == 0:
logger.info("Installing admin user")
role = Role.query.filter(Role.name == "Superuser").first()

View File

@ -104,7 +104,7 @@ def frontend(userid, current_session):
raise Forbidden
if request.method == "POST":
if request.content_length > 1024**2:
if request.content_length > 1024 ** 2:
raise BadRequest
current_session.user_.set_attribute("frontend", request.get_json())
return no_content()