Compare commits

...

4 Commits

Author SHA1 Message Date
Ferdinand Thiessen a289664f10 The enabled state of plugins is now loaded from database rather than config file
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-08-18 19:58:02 +02:00
Ferdinand Thiessen e41be21c47 Restructure models and database import paths
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-08-18 19:53:58 +02:00
Ferdinand Thiessen 7f8aa80b0e Update dependencies and increase python version to 3.10
Drop future imports, not needed with python 3.10

Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-08-18 19:45:54 +02:00
Ferdinand Thiessen dc2b949225 [cli] Fix exporting of plugin interfaces
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-07-31 19:28:11 +02:00
30 changed files with 335 additions and 261 deletions

View File

@ -4,14 +4,14 @@ from flask import Flask
from flask_cors import CORS
from datetime import datetime, date
from flask.json import JSONEncoder, jsonify
from importlib_metadata import entry_points
from importlib.metadata import entry_points
from sqlalchemy.exc import OperationalError
from werkzeug.exceptions import HTTPException
from flaschengeist import logger
from flaschengeist.models import Plugin
from flaschengeist.utils.hook import Hook
from flaschengeist.plugins import AuthPlugin
from flaschengeist.config import config, configure_app
from flaschengeist.config import configure_app
class CustomJSONEncoder(JSONEncoder):
@ -39,39 +39,30 @@ class CustomJSONEncoder(JSONEncoder):
def load_plugins(app: Flask):
app.config["FG_PLUGINS"] = {}
for entry_point in entry_points(group="flaschengeist.plugins"):
logger.debug(f"Found plugin: {entry_point.name} ({entry_point.dist.version})")
enabled_plugins = Plugin.query.filter(Plugin.enabled == True).all()
all_plugins = entry_points(group="flaschengeist.plugins")
if entry_point.name == config["FLASCHENGEIST"]["auth"] or (
entry_point.name in config and config[entry_point.name].get("enabled", False)
):
logger.debug(f"Load plugin {entry_point.name}")
try:
plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {}))
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
app.register_blueprint(plugin.blueprint)
except:
logger.error(
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.",
exc_info=True,
)
continue
if isinstance(plugin, AuthPlugin):
if entry_point.name != config["FLASCHENGEIST"]["auth"]:
logger.debug(f"Unload not configured AuthPlugin {entry_point.name}")
del plugin
continue
else:
logger.info(f"Using authentication plugin: {entry_point.name}")
app.config["FG_AUTH_BACKEND"] = plugin
else:
logger.info(f"Using plugin: {entry_point.name}")
app.config["FG_PLUGINS"][entry_point.name] = plugin
else:
logger.debug(f"Skip disabled plugin {entry_point.name}")
if "FG_AUTH_BACKEND" not in app.config:
logger.fatal("No authentication plugin configured or authentication plugin not found")
raise RuntimeError("No authentication plugin configured or authentication plugin not found")
for plugin in enabled_plugins:
logger.debug(f"Searching for enabled plugin {plugin.name}")
entry_point = all_plugins.select(name=plugin.name)
if not entry_point:
logger.error(
f"Plugin {plugin.name} was enabled, but could not be found.",
exc_info=True,
)
continue
try:
loaded = entry_point.load()(entry_point)
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
app.register_blueprint(plugin.blueprint)
except:
logger.error(
f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.",
exc_info=True,
)
continue
logger.info(f"Loaded plugin: {plugin.name}")
app.config["FG_PLUGINS"][plugin.name] = loaded
def create_app(test_config=None, cli=False):

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

@ -2,11 +2,11 @@ import click
from click.decorators import pass_context
from flask import current_app
from flask.cli import with_appcontext
from importlib_metadata import EntryPoint, entry_points
from importlib.metadata import EntryPoint, entry_points
from flaschengeist.database import db
from flaschengeist.config import config
from flaschengeist.models.user import Permission
from flaschengeist.models import Permission
@click.group()
@ -97,7 +97,9 @@ 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 and value["enabled"]] + [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

@ -77,17 +77,6 @@ def configure_app(app, test_config=None):
configure_logger()
# Always enable this builtin plugins!
update_dict(
config,
{
"auth": {"enabled": True},
"roles": {"enabled": True},
"users": {"enabled": True},
"scheduler": {"enabled": True},
},
)
if "secret_key" not in config["FLASCHENGEIST"]:
logger.critical("No secret key was configured, please configure one for production systems!")
raise RuntimeError("No secret key was configured")

View File

@ -1,15 +1,14 @@
from datetime import date
from flask import send_file
from pathlib import Path
from flask import send_file
from PIL import Image as PImage
from werkzeug.exceptions import NotFound, UnprocessableEntity
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import NotFound, UnprocessableEntity
from flaschengeist.models.image import Image
from flaschengeist.database import db
from flaschengeist.config import config
from ..models import Image
from ..database import db
from ..config import config
def check_mimetype(mime: str):

View File

@ -1,5 +1,5 @@
from flaschengeist.utils.hook import Hook
from flaschengeist.models.user import User, Role
from flaschengeist.models import User, Role
class Message:

View File

@ -4,9 +4,16 @@ Used by plugins for setting and notification functionality.
"""
import sqlalchemy
from typing import Union
from flask import current_app
from werkzeug.exceptions import NotFound
from importlib.metadata import entry_points
from .. import logger
from ..database import db
from ..models.setting import _PluginSetting
from ..models.notification import Notification
from ..utils import Hook
from ..models import Plugin, PluginSetting, Notification
def get_setting(plugin_id: str, name: str, **kwargs):
@ -23,7 +30,7 @@ def get_setting(plugin_id: str, name: str, **kwargs):
"""
try:
setting = (
_PluginSetting.query.filter(_PluginSetting.plugin == plugin_id).filter(_PluginSetting.name == name).one()
PluginSetting.query.filter(PluginSetting.plugin == plugin_id).filter(PluginSetting.name == name).one()
)
return setting.value
except sqlalchemy.orm.exc.NoResultFound:
@ -42,8 +49,8 @@ def set_setting(plugin_id: str, name: str, value):
value: Value to be stored
"""
setting = (
_PluginSetting.query.filter(_PluginSetting.plugin == plugin_id)
.filter(_PluginSetting.name == name)
PluginSetting.query.filter(PluginSetting.plugin == plugin_id)
.filter(PluginSetting.name == name)
.one_or_none()
)
if setting is not None:
@ -52,7 +59,7 @@ def set_setting(plugin_id: str, name: str, value):
else:
setting.value = value
else:
db.session.add(_PluginSetting(plugin=plugin_id, name=name, value=value))
db.session.add(PluginSetting(plugin=plugin_id, name=name, value=value))
db.session.commit()
@ -74,3 +81,64 @@ def notify(plugin_id: str, user, text: str, data=None):
db.session.add(n)
db.session.commit()
return n.id
@Hook("plugins.installed")
def install_plugin(plugin_name: str):
logger.debug(f"Installing plugin {plugin_name}")
entry_point = entry_points(group="flaschengeist.plugins", name=plugin_name)
if not entry_point:
raise NotFound
plugin = entry_point[0].load()(entry_point[0])
entity = Plugin(name=plugin.name, version=plugin.version)
db.session.add(entity)
db.session.commit()
return entity
@Hook("plugin.uninstalled")
def uninstall_plugin(plugin_id: Union[str, int]):
plugin = disable_plugin(plugin_id)
logger.debug(f"Uninstall plugin {plugin.name}")
entity = current_app.config["FG_PLUGINS"][plugin.name]
entity.uninstall()
del current_app.config["FG_PLUGINS"][plugin.name]
db.session.delete(plugin)
db.session.commit()
@Hook("plugins.enabled")
def enable_plugin(plugin_id: Union[str, int]):
logger.debug(f"Enabling plugin {plugin_id}")
plugin: Plugin = Plugin.query
if isinstance(plugin_id, str):
plugin = plugin.filter(Plugin.name == plugin_id).one_or_none()
if plugin is None:
logger.debug("Plugin not installed, trying to install")
plugin = install_plugin(plugin_id)
else:
plugin = plugin.get(plugin_id)
if plugin is None:
raise NotFound
plugin.enabled = True
db.session.commit()
return plugin
@Hook("plugins.disabled")
def disable_plugin(plugin_id: Union[str, int]):
logger.debug(f"Disabling plugin {plugin_id}")
plugin: Plugin = Plugin.query
if isinstance(plugin_id, str):
plugin = plugin.filter(Plugin.name == plugin_id).one_or_none()
else:
plugin = plugin.get(plugin_id)
if plugin is None:
raise NotFound
plugin.enabled = False
db.session.commit()
return plugin

View File

@ -2,10 +2,10 @@ from typing import Union
from sqlalchemy.exc import IntegrityError
from werkzeug.exceptions import BadRequest, Conflict, NotFound
from flaschengeist import logger
from flaschengeist.models.user import Role, Permission
from flaschengeist.database import db, case_sensitive
from flaschengeist.utils.hook import Hook
from .. import logger
from ..models import Role, Permission
from ..database import db, case_sensitive
from ..utils.hook import Hook
def get_all():

View File

@ -1,9 +1,12 @@
import secrets
from flaschengeist.models.session import Session
from flaschengeist.database import db
from flaschengeist import logger
from werkzeug.exceptions import Forbidden, Unauthorized
from datetime import datetime, timezone
from werkzeug.exceptions import Forbidden, Unauthorized
from .. import logger
from ..models import Session
from ..database import db
lifetime = 1800

View File

@ -1,5 +1,6 @@
import secrets
import re
import secrets
from io import BytesIO
from sqlalchemy import exc
from flask import current_app
@ -7,15 +8,15 @@ from datetime import datetime, timedelta, timezone
from flask.helpers import send_file
from werkzeug.exceptions import NotFound, BadRequest, Forbidden
from flaschengeist import logger
from flaschengeist.config import config
from flaschengeist.database import db
from flaschengeist.models.notification import Notification
from flaschengeist.utils.hook import Hook
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.utils.foreign_keys import merge_references
from flaschengeist.models.user import User, Role, _PasswordReset
from flaschengeist.controller import imageController, messageController, sessionController
from .. import logger
from ..config import config
from ..database import db
from ..models import Notification, User, Role
from ..models.user import _PasswordReset
from ..utils.hook import Hook
from ..utils.datetime import from_iso_format
from ..utils.foreign_keys import merge_references
from ..controller import imageController, messageController, sessionController
def __active_users():

View File

@ -1,7 +1,7 @@
import os
from flask_migrate import Migrate, Config
from flask_sqlalchemy import SQLAlchemy
from importlib_metadata import EntryPoint
from importlib.metadata import EntryPoint, entry_points, distribution
from sqlalchemy import MetaData
from flaschengeist import logger
@ -30,8 +30,6 @@ def configure_alembic(config: Config):
This includes even disabled plugins, as simply disabling a plugin without
uninstall can break the alembic version management.
"""
from importlib_metadata import entry_points, distribution
# Set main script location
config.set_main_option(
"script_location", str(distribution("flaschengeist").locate_file("") / "flaschengeist" / "alembic")

View File

@ -0,0 +1,95 @@
import sys
import datetime
from sqlalchemy import BigInteger, util
from sqlalchemy.dialects import mysql, sqlite
from sqlalchemy.types import DateTime, TypeDecorator
class ModelSerializeMixin:
"""Mixin class used for models to serialize them automatically
Ignores private and protected members as well as members marked as not to publish (name ends with _)
"""
def __is_optional(self, param):
if sys.version_info < (3, 8):
return False
import typing
hint = typing.get_type_hints(self.__class__)[param]
if (
typing.get_origin(hint) is typing.Union
and len(typing.get_args(hint)) == 2
and typing.get_args(hint)[1] is type(None)
):
return getattr(self, param) is None
def serialize(self):
"""Serialize class to dict
Returns:
Dict of all not private or protected annotated member variables.
"""
d = {
param: getattr(self, param)
for param in self.__class__.__annotations__
if not param.startswith("_") and not param.endswith("_") and not self.__is_optional(param)
}
if len(d) == 1:
key, value = d.popitem()
return value
return d
def __str__(self) -> str:
return self.serialize().__str__()
class Serial(TypeDecorator):
"""Same as MariaDB Serial used for IDs"""
cache_ok = True
impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql").with_variant(sqlite.INTEGER(), "sqlite")
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html?highlight=custom%20column#affecting-the-rendering-of-types-themselves
def __repr__(self) -> str:
return util.generic_repr(self)
class UtcDateTime(TypeDecorator):
"""Almost equivalent to `sqlalchemy.types.DateTime` with
``timezone=True`` option, but it differs from that by:
- Never silently take naive :class:`datetime.datetime`, instead it
always raise :exc:`ValueError` unless time zone aware value.
- :class:`datetime.datetime` value's :attr:`datetime.datetime.tzinfo`
is always converted to UTC.
- Unlike SQLAlchemy's built-in :class:`sqlalchemy.types.DateTime`,
it never return naive :class:`datetime.datetime`, but time zone
aware value, even with SQLite or MySQL.
"""
cache_ok = True
impl = DateTime(timezone=True)
@staticmethod
def current_utc():
return datetime.datetime.now(tz=datetime.timezone.utc)
def process_bind_param(self, value, dialect):
if value is not None:
if not isinstance(value, datetime.datetime):
raise TypeError("expected datetime.datetime, not " + repr(value))
elif value.tzinfo is None:
raise ValueError("naive datetime is disallowed")
return value.astimezone(datetime.timezone.utc)
def process_result_value(self, value, dialect):
if value is not None:
if value.tzinfo is not None:
value = value.astimezone(datetime.timezone.utc)
value = value.replace(tzinfo=datetime.timezone.utc)
return value
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html?highlight=custom%20column#affecting-the-rendering-of-types-themselves
def __repr__(self) -> str:
return util.generic_repr(self)

View File

@ -1,95 +1,5 @@
import sys
import datetime
from sqlalchemy import BigInteger, util
from sqlalchemy.dialects import mysql, sqlite
from sqlalchemy.types import DateTime, TypeDecorator
class ModelSerializeMixin:
"""Mixin class used for models to serialize them automatically
Ignores private and protected members as well as members marked as not to publish (name ends with _)
"""
def __is_optional(self, param):
if sys.version_info < (3, 8):
return False
import typing
hint = typing.get_type_hints(self.__class__)[param]
if (
typing.get_origin(hint) is typing.Union
and len(typing.get_args(hint)) == 2
and typing.get_args(hint)[1] is type(None)
):
return getattr(self, param) is None
def serialize(self):
"""Serialize class to dict
Returns:
Dict of all not private or protected annotated member variables.
"""
d = {
param: getattr(self, param)
for param in self.__class__.__annotations__
if not param.startswith("_") and not param.endswith("_") and not self.__is_optional(param)
}
if len(d) == 1:
key, value = d.popitem()
return value
return d
def __str__(self) -> str:
return self.serialize().__str__()
class Serial(TypeDecorator):
"""Same as MariaDB Serial used for IDs"""
cache_ok = True
impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql").with_variant(sqlite.INTEGER(), "sqlite")
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html?highlight=custom%20column#affecting-the-rendering-of-types-themselves
def __repr__(self) -> str:
return util.generic_repr(self)
class UtcDateTime(TypeDecorator):
"""Almost equivalent to `sqlalchemy.types.DateTime` with
``timezone=True`` option, but it differs from that by:
- Never silently take naive :class:`datetime.datetime`, instead it
always raise :exc:`ValueError` unless time zone aware value.
- :class:`datetime.datetime` value's :attr:`datetime.datetime.tzinfo`
is always converted to UTC.
- Unlike SQLAlchemy's built-in :class:`sqlalchemy.types.DateTime`,
it never return naive :class:`datetime.datetime`, but time zone
aware value, even with SQLite or MySQL.
"""
cache_ok = True
impl = DateTime(timezone=True)
@staticmethod
def current_utc():
return datetime.datetime.now(tz=datetime.timezone.utc)
def process_bind_param(self, value, dialect):
if value is not None:
if not isinstance(value, datetime.datetime):
raise TypeError("expected datetime.datetime, not " + repr(value))
elif value.tzinfo is None:
raise ValueError("naive datetime is disallowed")
return value.astimezone(datetime.timezone.utc)
def process_result_value(self, value, dialect):
if value is not None:
if value.tzinfo is not None:
value = value.astimezone(datetime.timezone.utc)
value = value.replace(tzinfo=datetime.timezone.utc)
return value
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html?highlight=custom%20column#affecting-the-rendering-of-types-themselves
def __repr__(self) -> str:
return util.generic_repr(self)
from .session import *
from .user import *
from .plugin import *
from .notification import *
from .image import *

View File

@ -1,10 +1,8 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from sqlalchemy import event
from pathlib import Path
from . import ModelSerializeMixin, Serial
from ..database import db
from ..database.types import ModelSerializeMixin, Serial
class Image(db.Model, ModelSerializeMixin):

View File

@ -1,19 +1,24 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime
from typing import Any
from . import Serial, UtcDateTime, ModelSerializeMixin
from ..database import db
from .user import User
from ..database.types import Serial, UtcDateTime, ModelSerializeMixin
class Notification(db.Model, ModelSerializeMixin):
__tablename__ = "notification"
id: int = db.Column("id", Serial, primary_key=True)
plugin: str = db.Column(db.String(127), nullable=False)
text: str = db.Column(db.Text)
data: Any = db.Column(db.PickleType(protocol=4))
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
user_id_: int = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False)
user_: User = db.relationship("User")
user_id_: int = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"), nullable=False)
user_: "User" = db.relationship("User")
plugin_: "Plugin" = db.relationship(
"Plugin", backref=db.backref("notifications_", cascade="all, delete, delete-orphan")
)
@property
def plugin(self):
return self.plugin_.name

View File

@ -0,0 +1,24 @@
from typing import Any
from ..database import db
from ..database.types import Serial
class Plugin(db.Model):
__tablename__ = "plugin"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(127), nullable=False)
version: str = db.Column(db.String(30), nullable=False)
"""The latest installed version"""
enabled: bool = db.Column(db.Boolean, default=False)
settings_ = db.relationship("PluginSetting", cascade="all, delete")
permissions_ = db.relationship("Permission", cascade="all, delete")
class PluginSetting(db.Model):
__tablename__ = "plugin_setting"
id = db.Column("id", Serial, primary_key=True)
plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
name: str = db.Column(db.String(127), nullable=False)
value: Any = db.Column(db.PickleType(protocol=4))

View File

@ -1,11 +1,9 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime, timedelta, timezone
from . import ModelSerializeMixin, UtcDateTime, Serial
from .user import User
from flaschengeist.database import db
from secrets import compare_digest
from ..database import db
from ..database.types import ModelSerializeMixin, UtcDateTime, Serial
from flaschengeist import logger
@ -28,7 +26,7 @@ class Session(db.Model, ModelSerializeMixin):
_id = db.Column("id", Serial, primary_key=True)
_user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"))
user_: User = db.relationship("User", back_populates="sessions_")
user_: "User" = db.relationship("User", back_populates="sessions_")
@property
def userid(self):

View File

@ -1,13 +0,0 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from typing import Any
from . import Serial
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(127))
name: str = db.Column(db.String(127), nullable=False)
value: Any = db.Column(db.PickleType(protocol=4))

View File

@ -1,14 +1,9 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from flask import url_for
from typing import Optional
from datetime import date, datetime
from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db
from . import ModelSerializeMixin, UtcDateTime, Serial
from .image import Image
from ..database.types import ModelSerializeMixin, UtcDateTime, Serial
association_table = db.Table(
"user_x_role",
@ -22,12 +17,12 @@ role_permission_association_table = db.Table(
db.Column("permission_id", Serial, db.ForeignKey("permission.id")),
)
class Permission(db.Model, ModelSerializeMixin):
__tablename__ = "permission"
name: str = db.Column(db.String(30), unique=True)
_id = db.Column("id", Serial, primary_key=True)
_plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
class Role(db.Model, ModelSerializeMixin):
@ -69,7 +64,7 @@ class User(db.Model, ModelSerializeMixin):
sessions_: list["Session"] = db.relationship(
"Session", back_populates="user_", cascade="all, delete, delete-orphan"
)
avatar_: Optional[Image] = db.relationship("Image", cascade="all, delete, delete-orphan", single_parent=True)
avatar_: Optional["Image"] = db.relationship("Image", cascade="all, delete, delete-orphan", single_parent=True)
reset_requests_: list["_PasswordReset"] = db.relationship("_PasswordReset", cascade="all, delete, delete-orphan")
# Private stuff for internal use

View File

@ -5,11 +5,12 @@
"""
from typing import Optional
from importlib_metadata import Distribution, EntryPoint
from importlib.metadata import Distribution, EntryPoint
from werkzeug.exceptions import MethodNotAllowed, NotFound
from werkzeug.datastructures import FileStorage
from flaschengeist.models.user import _Avatar, User
from flaschengeist.models import User
from flaschengeist.models.user import _Avatar
from flaschengeist.utils.hook import HookBefore, HookAfter
__all__ = [
@ -19,7 +20,7 @@ __all__ = [
"before_role_updated",
"before_update_user",
"after_role_updated",
"Plugin",
"BasePlugin",
"AuthPlugin",
]
@ -70,7 +71,7 @@ Passed args:
"""
class Plugin:
class BasePlugin:
"""Base class for all Plugins
All plugins must derived from this class.
@ -114,10 +115,10 @@ class Plugin:
```
"""
def __init__(self, entry_point: EntryPoint, config=None):
def __init__(self, entry_point: EntryPoint):
"""Constructor called by create_app
Args:
config: Dict configuration containing the plugin section
entry_point: EntryPoint from which this plugin was loaded
"""
self.version = entry_point.dist.version
self.name = entry_point.name
@ -126,6 +127,8 @@ class Plugin:
def install(self):
"""Installation routine
Also called when updating the plugin, compare `version` and `installed_version`.
Is always called with Flask application context,
it is called after the plugin permissions are installed.
"""
@ -142,6 +145,14 @@ class Plugin:
"""
pass
@property
def installed_version(self):
"""Installed version of the plugin"""
from ..controller import pluginController
self.__installed_version = pluginController.get_installed_version(self.name)
return self.__installed_version
def get_setting(self, name: str, **kwargs):
"""Get plugin setting from database
@ -193,10 +204,10 @@ class Plugin:
return {"version": self.version, "permissions": self.permissions}
class AuthPlugin(Plugin):
class AuthPlugin(BasePlugin):
"""Base class for all authentification plugins
See also `Plugin`
See also `BasePlugin`
"""
def login(self, user, pw):

View File

@ -6,13 +6,13 @@ from flask import Blueprint, request, jsonify
from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized
from flaschengeist import logger
from flaschengeist.plugins import Plugin
from flaschengeist.plugins import BasePlugin
from flaschengeist.utils.HTTP import no_content, created
from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import sessionController, userController
class AuthRoutePlugin(Plugin):
class AuthRoutePlugin(BasePlugin):
blueprint = Blueprint("auth", __name__)

View File

@ -12,7 +12,8 @@ from werkzeug.datastructures import FileStorage
from flaschengeist import logger
from flaschengeist.controller import userController
from flaschengeist.models.user import User, Role, _Avatar
from flaschengeist.models import User, Role
from flaschengeist.models.user import _Avatar
from flaschengeist.plugins import AuthPlugin, before_role_updated

View File

@ -8,7 +8,7 @@ import hashlib
import binascii
from werkzeug.exceptions import BadRequest
from flaschengeist.plugins import AuthPlugin, plugins_installed
from flaschengeist.models.user import User, Role, Permission
from flaschengeist.models import User, Role, Permission
from flaschengeist.database import db
from flaschengeist import logger

View File

@ -1,5 +1,3 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime
from typing import Optional
from sqlalchemy.ext.hybrid import hybrid_property

View File

@ -3,15 +3,14 @@ from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from flaschengeist import logger
from flaschengeist.models.user import User
from flaschengeist.models import User
from flaschengeist.plugins import BasePlugin
from flaschengeist.utils.hook import HookAfter
from flaschengeist.controller import userController
from flaschengeist.controller.messageController import Message
from . import Plugin
class MailMessagePlugin(Plugin):
class MailMessagePlugin(BasePlugin):
def __init__(self, entry_point, config):
super().__init__(entry_point, config)
self.server = config["SERVER"]

View File

@ -1,8 +1,6 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from flaschengeist.database import db
from flaschengeist.models import ModelSerializeMixin, Serial
from flaschengeist.models.image import Image
from flaschengeist.database.types import ModelSerializeMixin, Serial
from flaschengeist.models import Image
from typing import Optional

View File

@ -5,17 +5,16 @@ Provides routes used to configure roles and permissions of users / roles.
from werkzeug.exceptions import BadRequest
from flask import Blueprint, request, jsonify
from http.client import NO_CONTENT
from flaschengeist.plugins import Plugin
from flaschengeist.utils.decorators import login_required
from flaschengeist.plugins import BasePlugin
from flaschengeist.controller import roleController
from flaschengeist.utils.HTTP import created, no_content
from flaschengeist.utils.decorators import login_required
from . import permissions
class RolesPlugin(Plugin):
class RolesPlugin(BasePlugin):
blueprint = Blueprint("roles", __name__)
permissions = permissions.permissions

View File

@ -2,10 +2,9 @@ from flask import Blueprint
from datetime import datetime, timedelta
from flaschengeist import logger
from flaschengeist.plugins import BasePlugin
from flaschengeist.utils.HTTP import no_content
from . import Plugin
class __Task:
def __init__(self, function, **kwags):
@ -38,7 +37,7 @@ def scheduled(id: str, replace=False, **kwargs):
return real_decorator
class SchedulerPlugin(Plugin):
class SchedulerPlugin(BasePlugin):
def __init__(self, entry_point, config=None):
super().__init__(entry_point, config)
self.blueprint = Blueprint(self.name, __name__)

View File

@ -2,14 +2,14 @@
Provides routes used to manage users
"""
from http.client import NO_CONTENT, CREATED
from http.client import CREATED
from flask import Blueprint, request, jsonify, make_response
from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed, NotFound
from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed
from . import permissions
from flaschengeist import logger
from flaschengeist.config import config
from flaschengeist.plugins import Plugin
from flaschengeist.plugins import BasePlugin
from flaschengeist.models.user import User
from flaschengeist.utils.decorators import login_required, extract_session, headers
from flaschengeist.controller import userController
@ -17,7 +17,7 @@ from flaschengeist.utils.HTTP import created, no_content
from flaschengeist.utils.datetime import from_iso_format
class UsersPlugin(Plugin):
class UsersPlugin(BasePlugin):
blueprint = Blueprint("users", __name__)
permissions = permissions.permissions

View File

@ -19,19 +19,17 @@ classifiers =
[options]
include_package_data = True
python_requires = >=3.9
python_requires = >=3.10
packages = find:
install_requires =
Flask>=2.0
Pillow>=8.4.0
Flask==2.0.3
Pillow>=9.0
flask_cors
flask_migrate>=3.1.0
flask_sqlalchemy>=2.5
# Importlib requirement can be dropped when python requirement is >= 3.10
importlib_metadata>=4.3
sqlalchemy>=1.4.26
flask_sqlalchemy>=2.5.1
sqlalchemy>=1.4.39
toml
werkzeug >= 2.0
werkzeug==2.0.3
[options.extras_require]
argon = argon2-cffi