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 flask_cors import CORS
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from flask.json import JSONEncoder, jsonify
|
from flask.json import JSONEncoder, jsonify
|
||||||
from importlib_metadata import entry_points
|
from importlib.metadata import entry_points
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
from flaschengeist.models import Plugin
|
||||||
from flaschengeist.utils.hook import Hook
|
from flaschengeist.utils.hook import Hook
|
||||||
from flaschengeist.plugins import AuthPlugin
|
from flaschengeist.config import configure_app
|
||||||
from flaschengeist.config import config, configure_app
|
|
||||||
|
|
||||||
|
|
||||||
class CustomJSONEncoder(JSONEncoder):
|
class CustomJSONEncoder(JSONEncoder):
|
||||||
|
@ -39,39 +39,30 @@ class CustomJSONEncoder(JSONEncoder):
|
||||||
def load_plugins(app: Flask):
|
def load_plugins(app: Flask):
|
||||||
app.config["FG_PLUGINS"] = {}
|
app.config["FG_PLUGINS"] = {}
|
||||||
|
|
||||||
for entry_point in entry_points(group="flaschengeist.plugins"):
|
enabled_plugins = Plugin.query.filter(Plugin.enabled == True).all()
|
||||||
logger.debug(f"Found plugin: {entry_point.name} ({entry_point.dist.version})")
|
all_plugins = entry_points(group="flaschengeist.plugins")
|
||||||
|
|
||||||
if entry_point.name == config["FLASCHENGEIST"]["auth"] or (
|
for plugin in enabled_plugins:
|
||||||
entry_point.name in config and config[entry_point.name].get("enabled", False)
|
logger.debug(f"Searching for enabled plugin {plugin.name}")
|
||||||
):
|
entry_point = all_plugins.select(name=plugin.name)
|
||||||
logger.debug(f"Load plugin {entry_point.name}")
|
if not entry_point:
|
||||||
try:
|
logger.error(
|
||||||
plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {}))
|
f"Plugin {plugin.name} was enabled, but could not be found.",
|
||||||
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
|
exc_info=True,
|
||||||
app.register_blueprint(plugin.blueprint)
|
)
|
||||||
except:
|
continue
|
||||||
logger.error(
|
try:
|
||||||
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.",
|
loaded = entry_point.load()(entry_point)
|
||||||
exc_info=True,
|
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
|
||||||
)
|
app.register_blueprint(plugin.blueprint)
|
||||||
continue
|
except:
|
||||||
if isinstance(plugin, AuthPlugin):
|
logger.error(
|
||||||
if entry_point.name != config["FLASCHENGEIST"]["auth"]:
|
f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.",
|
||||||
logger.debug(f"Unload not configured AuthPlugin {entry_point.name}")
|
exc_info=True,
|
||||||
del plugin
|
)
|
||||||
continue
|
continue
|
||||||
else:
|
logger.info(f"Loaded plugin: {plugin.name}")
|
||||||
logger.info(f"Using authentication plugin: {entry_point.name}")
|
app.config["FG_PLUGINS"][plugin.name] = loaded
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(test_config=None, cli=False):
|
def create_app(test_config=None, cli=False):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import click
|
import click
|
||||||
from importlib_metadata import entry_points
|
from importlib.metadata import entry_points
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
|
|
@ -2,11 +2,11 @@ import click
|
||||||
from click.decorators import pass_context
|
from click.decorators import pass_context
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.cli import with_appcontext
|
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.database import db
|
||||||
from flaschengeist.config import config
|
from flaschengeist.config import config
|
||||||
from flaschengeist.models.user import Permission
|
from flaschengeist.models import Permission
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
@ -97,7 +97,9 @@ def ls(enabled, no_header):
|
||||||
return p.version
|
return p.version
|
||||||
|
|
||||||
plugins = entry_points(group="flaschengeist.plugins")
|
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()
|
loaded_plugins = current_app.config["FG_PLUGINS"].keys()
|
||||||
|
|
||||||
if not no_header:
|
if not no_header:
|
||||||
|
|
|
@ -77,17 +77,6 @@ def configure_app(app, test_config=None):
|
||||||
|
|
||||||
configure_logger()
|
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"]:
|
if "secret_key" not in config["FLASCHENGEIST"]:
|
||||||
logger.critical("No secret key was configured, please configure one for production systems!")
|
logger.critical("No secret key was configured, please configure one for production systems!")
|
||||||
raise RuntimeError("No secret key was configured")
|
raise RuntimeError("No secret key was configured")
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from flask import send_file
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from flask import send_file
|
||||||
from PIL import Image as PImage
|
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.utils import secure_filename
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
from werkzeug.exceptions import NotFound, UnprocessableEntity
|
||||||
|
|
||||||
from flaschengeist.models.image import Image
|
from ..models import Image
|
||||||
from flaschengeist.database import db
|
from ..database import db
|
||||||
from flaschengeist.config import config
|
from ..config import config
|
||||||
|
|
||||||
|
|
||||||
def check_mimetype(mime: str):
|
def check_mimetype(mime: str):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from flaschengeist.utils.hook import Hook
|
from flaschengeist.utils.hook import Hook
|
||||||
from flaschengeist.models.user import User, Role
|
from flaschengeist.models import User, Role
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
class Message:
|
||||||
|
|
|
@ -4,9 +4,16 @@ Used by plugins for setting and notification functionality.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlalchemy
|
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 ..database import db
|
||||||
from ..models.setting import _PluginSetting
|
from ..utils import Hook
|
||||||
from ..models.notification import Notification
|
from ..models import Plugin, PluginSetting, Notification
|
||||||
|
|
||||||
|
|
||||||
def get_setting(plugin_id: str, name: str, **kwargs):
|
def get_setting(plugin_id: str, name: str, **kwargs):
|
||||||
|
@ -23,7 +30,7 @@ def get_setting(plugin_id: str, name: str, **kwargs):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
setting = (
|
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
|
return setting.value
|
||||||
except sqlalchemy.orm.exc.NoResultFound:
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
|
@ -42,8 +49,8 @@ def set_setting(plugin_id: str, name: str, value):
|
||||||
value: Value to be stored
|
value: Value to be stored
|
||||||
"""
|
"""
|
||||||
setting = (
|
setting = (
|
||||||
_PluginSetting.query.filter(_PluginSetting.plugin == plugin_id)
|
PluginSetting.query.filter(PluginSetting.plugin == plugin_id)
|
||||||
.filter(_PluginSetting.name == name)
|
.filter(PluginSetting.name == name)
|
||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
if setting is not None:
|
if setting is not None:
|
||||||
|
@ -52,7 +59,7 @@ def set_setting(plugin_id: str, name: str, value):
|
||||||
else:
|
else:
|
||||||
setting.value = value
|
setting.value = value
|
||||||
else:
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,3 +81,64 @@ def notify(plugin_id: str, user, text: str, data=None):
|
||||||
db.session.add(n)
|
db.session.add(n)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return n.id
|
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 sqlalchemy.exc import IntegrityError
|
||||||
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
||||||
|
|
||||||
from flaschengeist import logger
|
from .. import logger
|
||||||
from flaschengeist.models.user import Role, Permission
|
from ..models import Role, Permission
|
||||||
from flaschengeist.database import db, case_sensitive
|
from ..database import db, case_sensitive
|
||||||
from flaschengeist.utils.hook import Hook
|
from ..utils.hook import Hook
|
||||||
|
|
||||||
|
|
||||||
def get_all():
|
def get_all():
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import secrets
|
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 datetime import datetime, timezone
|
||||||
|
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||||
|
|
||||||
|
from .. import logger
|
||||||
|
from ..models import Session
|
||||||
|
from ..database import db
|
||||||
|
|
||||||
|
|
||||||
lifetime = 1800
|
lifetime = 1800
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import secrets
|
|
||||||
import re
|
import re
|
||||||
|
import secrets
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
@ -7,15 +8,15 @@ from datetime import datetime, timedelta, timezone
|
||||||
from flask.helpers import send_file
|
from flask.helpers import send_file
|
||||||
from werkzeug.exceptions import NotFound, BadRequest, Forbidden
|
from werkzeug.exceptions import NotFound, BadRequest, Forbidden
|
||||||
|
|
||||||
from flaschengeist import logger
|
from .. import logger
|
||||||
from flaschengeist.config import config
|
from ..config import config
|
||||||
from flaschengeist.database import db
|
from ..database import db
|
||||||
from flaschengeist.models.notification import Notification
|
from ..models import Notification, User, Role
|
||||||
from flaschengeist.utils.hook import Hook
|
from ..models.user import _PasswordReset
|
||||||
from flaschengeist.utils.datetime import from_iso_format
|
from ..utils.hook import Hook
|
||||||
from flaschengeist.utils.foreign_keys import merge_references
|
from ..utils.datetime import from_iso_format
|
||||||
from flaschengeist.models.user import User, Role, _PasswordReset
|
from ..utils.foreign_keys import merge_references
|
||||||
from flaschengeist.controller import imageController, messageController, sessionController
|
from ..controller import imageController, messageController, sessionController
|
||||||
|
|
||||||
|
|
||||||
def __active_users():
|
def __active_users():
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
from flask_migrate import Migrate, Config
|
from flask_migrate import Migrate, Config
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from importlib_metadata import EntryPoint
|
from importlib.metadata import EntryPoint, entry_points, distribution
|
||||||
from sqlalchemy import MetaData
|
from sqlalchemy import MetaData
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
@ -30,8 +30,6 @@ def configure_alembic(config: Config):
|
||||||
This includes even disabled plugins, as simply disabling a plugin without
|
This includes even disabled plugins, as simply disabling a plugin without
|
||||||
uninstall can break the alembic version management.
|
uninstall can break the alembic version management.
|
||||||
"""
|
"""
|
||||||
from importlib_metadata import entry_points, distribution
|
|
||||||
|
|
||||||
# Set main script location
|
# Set main script location
|
||||||
config.set_main_option(
|
config.set_main_option(
|
||||||
"script_location", str(distribution("flaschengeist").locate_file("") / "flaschengeist" / "alembic")
|
"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
|
from .session import *
|
||||||
import datetime
|
from .user import *
|
||||||
|
from .plugin import *
|
||||||
from sqlalchemy import BigInteger, util
|
from .notification import *
|
||||||
from sqlalchemy.dialects import mysql, sqlite
|
from .image import *
|
||||||
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,10 +1,8 @@
|
||||||
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
|
||||||
|
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import ModelSerializeMixin, Serial
|
|
||||||
from ..database import db
|
from ..database import db
|
||||||
|
from ..database.types import ModelSerializeMixin, Serial
|
||||||
|
|
||||||
|
|
||||||
class Image(db.Model, ModelSerializeMixin):
|
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 datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from . import Serial, UtcDateTime, ModelSerializeMixin
|
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from .user import User
|
from ..database.types import Serial, UtcDateTime, ModelSerializeMixin
|
||||||
|
|
||||||
|
|
||||||
class Notification(db.Model, ModelSerializeMixin):
|
class Notification(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "notification"
|
__tablename__ = "notification"
|
||||||
id: int = db.Column("id", Serial, primary_key=True)
|
id: int = db.Column("id", Serial, primary_key=True)
|
||||||
plugin: str = db.Column(db.String(127), nullable=False)
|
|
||||||
text: str = db.Column(db.Text)
|
text: str = db.Column(db.Text)
|
||||||
data: Any = db.Column(db.PickleType(protocol=4))
|
data: Any = db.Column(db.PickleType(protocol=4))
|
||||||
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
|
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_id_: int = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
user_: User = db.relationship("User")
|
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 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 secrets import compare_digest
|
||||||
|
|
||||||
|
from ..database import db
|
||||||
|
from ..database.types import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,7 +26,7 @@ class Session(db.Model, ModelSerializeMixin):
|
||||||
|
|
||||||
_id = db.Column("id", Serial, primary_key=True)
|
_id = db.Column("id", Serial, primary_key=True)
|
||||||
_user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"))
|
_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
|
@property
|
||||||
def userid(self):
|
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 typing import Optional
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from sqlalchemy.orm.collections import attribute_mapped_collection
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||||
|
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from . import ModelSerializeMixin, UtcDateTime, Serial
|
from ..database.types import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
from .image import Image
|
|
||||||
|
|
||||||
|
|
||||||
association_table = db.Table(
|
association_table = db.Table(
|
||||||
"user_x_role",
|
"user_x_role",
|
||||||
|
@ -22,12 +17,12 @@ role_permission_association_table = db.Table(
|
||||||
db.Column("permission_id", Serial, db.ForeignKey("permission.id")),
|
db.Column("permission_id", Serial, db.ForeignKey("permission.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Permission(db.Model, ModelSerializeMixin):
|
class Permission(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "permission"
|
__tablename__ = "permission"
|
||||||
name: str = db.Column(db.String(30), unique=True)
|
name: str = db.Column(db.String(30), unique=True)
|
||||||
|
|
||||||
_id = db.Column("id", Serial, primary_key=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):
|
class Role(db.Model, ModelSerializeMixin):
|
||||||
|
@ -69,7 +64,7 @@ class User(db.Model, ModelSerializeMixin):
|
||||||
sessions_: list["Session"] = db.relationship(
|
sessions_: list["Session"] = db.relationship(
|
||||||
"Session", back_populates="user_", cascade="all, delete, delete-orphan"
|
"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")
|
reset_requests_: list["_PasswordReset"] = db.relationship("_PasswordReset", cascade="all, delete, delete-orphan")
|
||||||
|
|
||||||
# Private stuff for internal use
|
# Private stuff for internal use
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from importlib_metadata import Distribution, EntryPoint
|
from importlib.metadata import Distribution, EntryPoint
|
||||||
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||||
from werkzeug.datastructures import FileStorage
|
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
|
from flaschengeist.utils.hook import HookBefore, HookAfter
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -19,7 +20,7 @@ __all__ = [
|
||||||
"before_role_updated",
|
"before_role_updated",
|
||||||
"before_update_user",
|
"before_update_user",
|
||||||
"after_role_updated",
|
"after_role_updated",
|
||||||
"Plugin",
|
"BasePlugin",
|
||||||
"AuthPlugin",
|
"AuthPlugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ Passed args:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class BasePlugin:
|
||||||
"""Base class for all Plugins
|
"""Base class for all Plugins
|
||||||
|
|
||||||
All plugins must derived from this class.
|
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
|
"""Constructor called by create_app
|
||||||
Args:
|
Args:
|
||||||
config: Dict configuration containing the plugin section
|
entry_point: EntryPoint from which this plugin was loaded
|
||||||
"""
|
"""
|
||||||
self.version = entry_point.dist.version
|
self.version = entry_point.dist.version
|
||||||
self.name = entry_point.name
|
self.name = entry_point.name
|
||||||
|
@ -126,6 +127,8 @@ class Plugin:
|
||||||
def install(self):
|
def install(self):
|
||||||
"""Installation routine
|
"""Installation routine
|
||||||
|
|
||||||
|
Also called when updating the plugin, compare `version` and `installed_version`.
|
||||||
|
|
||||||
Is always called with Flask application context,
|
Is always called with Flask application context,
|
||||||
it is called after the plugin permissions are installed.
|
it is called after the plugin permissions are installed.
|
||||||
"""
|
"""
|
||||||
|
@ -142,6 +145,14 @@ class Plugin:
|
||||||
"""
|
"""
|
||||||
pass
|
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):
|
def get_setting(self, name: str, **kwargs):
|
||||||
"""Get plugin setting from database
|
"""Get plugin setting from database
|
||||||
|
|
||||||
|
@ -193,10 +204,10 @@ class Plugin:
|
||||||
return {"version": self.version, "permissions": self.permissions}
|
return {"version": self.version, "permissions": self.permissions}
|
||||||
|
|
||||||
|
|
||||||
class AuthPlugin(Plugin):
|
class AuthPlugin(BasePlugin):
|
||||||
"""Base class for all authentification plugins
|
"""Base class for all authentification plugins
|
||||||
|
|
||||||
See also `Plugin`
|
See also `BasePlugin`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def login(self, user, pw):
|
def login(self, user, pw):
|
||||||
|
|
|
@ -6,13 +6,13 @@ from flask import Blueprint, request, jsonify
|
||||||
from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized
|
from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized
|
||||||
|
|
||||||
from flaschengeist import logger
|
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.HTTP import no_content, created
|
||||||
from flaschengeist.utils.decorators import login_required
|
from flaschengeist.utils.decorators import login_required
|
||||||
from flaschengeist.controller import sessionController, userController
|
from flaschengeist.controller import sessionController, userController
|
||||||
|
|
||||||
|
|
||||||
class AuthRoutePlugin(Plugin):
|
class AuthRoutePlugin(BasePlugin):
|
||||||
blueprint = Blueprint("auth", __name__)
|
blueprint = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@ from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.controller import userController
|
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
|
from flaschengeist.plugins import AuthPlugin, before_role_updated
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import hashlib
|
||||||
import binascii
|
import binascii
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from flaschengeist.plugins import AuthPlugin, plugins_installed
|
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.database import db
|
||||||
from flaschengeist import logger
|
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 datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
|
|
@ -3,15 +3,14 @@ from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
from flaschengeist import logger
|
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.utils.hook import HookAfter
|
||||||
from flaschengeist.controller import userController
|
from flaschengeist.controller import userController
|
||||||
from flaschengeist.controller.messageController import Message
|
from flaschengeist.controller.messageController import Message
|
||||||
|
|
||||||
from . import Plugin
|
|
||||||
|
|
||||||
|
class MailMessagePlugin(BasePlugin):
|
||||||
class MailMessagePlugin(Plugin):
|
|
||||||
def __init__(self, entry_point, config):
|
def __init__(self, entry_point, config):
|
||||||
super().__init__(entry_point, config)
|
super().__init__(entry_point, config)
|
||||||
self.server = config["SERVER"]
|
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.database import db
|
||||||
from flaschengeist.models import ModelSerializeMixin, Serial
|
from flaschengeist.database.types import ModelSerializeMixin, Serial
|
||||||
from flaschengeist.models.image import Image
|
from flaschengeist.models import Image
|
||||||
|
|
||||||
from typing import Optional
|
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 werkzeug.exceptions import BadRequest
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from http.client import NO_CONTENT
|
|
||||||
|
|
||||||
from flaschengeist.plugins import Plugin
|
from flaschengeist.plugins import BasePlugin
|
||||||
from flaschengeist.utils.decorators import login_required
|
|
||||||
from flaschengeist.controller import roleController
|
from flaschengeist.controller import roleController
|
||||||
from flaschengeist.utils.HTTP import created, no_content
|
from flaschengeist.utils.HTTP import created, no_content
|
||||||
|
from flaschengeist.utils.decorators import login_required
|
||||||
|
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
class RolesPlugin(Plugin):
|
class RolesPlugin(BasePlugin):
|
||||||
blueprint = Blueprint("roles", __name__)
|
blueprint = Blueprint("roles", __name__)
|
||||||
permissions = permissions.permissions
|
permissions = permissions.permissions
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,9 @@ from flask import Blueprint
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
from flaschengeist.plugins import BasePlugin
|
||||||
from flaschengeist.utils.HTTP import no_content
|
from flaschengeist.utils.HTTP import no_content
|
||||||
|
|
||||||
from . import Plugin
|
|
||||||
|
|
||||||
|
|
||||||
class __Task:
|
class __Task:
|
||||||
def __init__(self, function, **kwags):
|
def __init__(self, function, **kwags):
|
||||||
|
@ -38,7 +37,7 @@ def scheduled(id: str, replace=False, **kwargs):
|
||||||
return real_decorator
|
return real_decorator
|
||||||
|
|
||||||
|
|
||||||
class SchedulerPlugin(Plugin):
|
class SchedulerPlugin(BasePlugin):
|
||||||
def __init__(self, entry_point, config=None):
|
def __init__(self, entry_point, config=None):
|
||||||
super().__init__(entry_point, config)
|
super().__init__(entry_point, config)
|
||||||
self.blueprint = Blueprint(self.name, __name__)
|
self.blueprint = Blueprint(self.name, __name__)
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
Provides routes used to manage users
|
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 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 . import permissions
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.config import config
|
from flaschengeist.config import config
|
||||||
from flaschengeist.plugins import Plugin
|
from flaschengeist.plugins import BasePlugin
|
||||||
from flaschengeist.models.user import User
|
from flaschengeist.models.user import User
|
||||||
from flaschengeist.utils.decorators import login_required, extract_session, headers
|
from flaschengeist.utils.decorators import login_required, extract_session, headers
|
||||||
from flaschengeist.controller import userController
|
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
|
from flaschengeist.utils.datetime import from_iso_format
|
||||||
|
|
||||||
|
|
||||||
class UsersPlugin(Plugin):
|
class UsersPlugin(BasePlugin):
|
||||||
blueprint = Blueprint("users", __name__)
|
blueprint = Blueprint("users", __name__)
|
||||||
permissions = permissions.permissions
|
permissions = permissions.permissions
|
||||||
|
|
||||||
|
|
14
setup.cfg
14
setup.cfg
|
@ -19,19 +19,17 @@ classifiers =
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
python_requires = >=3.9
|
python_requires = >=3.10
|
||||||
packages = find:
|
packages = find:
|
||||||
install_requires =
|
install_requires =
|
||||||
Flask>=2.0
|
Flask==2.0.3
|
||||||
Pillow>=8.4.0
|
Pillow>=9.0
|
||||||
flask_cors
|
flask_cors
|
||||||
flask_migrate>=3.1.0
|
flask_migrate>=3.1.0
|
||||||
flask_sqlalchemy>=2.5
|
flask_sqlalchemy>=2.5.1
|
||||||
# Importlib requirement can be dropped when python requirement is >= 3.10
|
sqlalchemy>=1.4.39
|
||||||
importlib_metadata>=4.3
|
|
||||||
sqlalchemy>=1.4.26
|
|
||||||
toml
|
toml
|
||||||
werkzeug >= 2.0
|
werkzeug==2.0.3
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
argon = argon2-cffi
|
argon = argon2-cffi
|
||||||
|
|
Loading…
Reference in New Issue