Compare commits
4 Commits
2880076705
...
a289664f10
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | a289664f10 | |
Ferdinand Thiessen | e41be21c47 | |
Ferdinand Thiessen | 7f8aa80b0e | |
Ferdinand Thiessen | dc2b949225 |
|
@ -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}")
|
||||
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:
|
||||
plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {}))
|
||||
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 {entry_point.name} was enabled, but could not be loaded due to an error.",
|
||||
f"Plugin {plugin.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")
|
||||
logger.info(f"Loaded plugin: {plugin.name}")
|
||||
app.config["FG_PLUGINS"][plugin.name] = loaded
|
||||
|
||||
|
||||
def create_app(test_config=None, cli=False):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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")
|
|
@ -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)
|
|
@ -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 *
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
14
setup.cfg
14
setup.cfg
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue