diff --git a/README.md b/README.md index 08aa09f..1cd3901 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,35 @@ or if you want to also run the tests: pip3 install --user ".[ldap,tests]" -You will also need a MySQL driver, recommended drivers are -- `mysqlclient` -- `PyMySQL` +You will also need a MySQL driver, by default one of this is installed: +- `mysqlclient` (non Windows) +- `PyMySQL` (on Windows) -`setup.py` will try to install a matching driver. +#### Hint on MySQL driver on Windows: +If you want to use `mysqlclient` instead of `PyMySQL` (performance?) you have to follow [this guide](https://www.radishlogic.com/coding/python-3/installing-mysqldb-for-python-3-in-windows/) -#### Windows -Same as above, but if you want to use `mysqlclient` instead of `PyMySQL` (performance?) you have to follow this guide: +### Install database +The user needs to have full permissions to the database. +If not you need to create user and database manually do (or similar on Windows): -https://www.radishlogic.com/coding/python-3/installing-mysqldb-for-python-3-in-windows/ + ( + echo "CREATE DATABASE flaschengeist;" + echo "CREATE USER 'flaschengeist'@'localhost' IDENTIFIED BY 'flaschengeist';" + echo "GRANT ALL PRIVILEGES ON flaschengeist.* TO 'flaschengeist'@'localhost';" + echo "FLUSH PRIVILEGES;" + ) | sudo mysql -### Configuration +Then you can install the database tables, this will update all tables from core + all enabled plugins. +*Hint:* The same command can be later used to upgrade the database after plugins or core are updated. + + $ flaschengeist db upgrade heads + +## Plugins +To only upgrade one plugin (for example the `events` plugin): + + $ flaschengeist db upgrade events@head + +## Configuration Configuration is done within the a `flaschengeist.toml`file, you can copy the one located inside the module path (where flaschegeist is installed) or create an empty one and place it inside either: 1. `~/.config/` @@ -63,21 +80,6 @@ So you have to configure one of the following options to call flaschengeists CRO - Pros: Guaranteed execution interval, no impact on user experience (at least if you do not limit wsgi worker threads) - Cons: Uses one of the webserver threads while executing -### Database installation -The user needs to have full permissions to the database. -If not you need to create user and database manually do (or similar on Windows): - - ( - echo "CREATE DATABASE flaschengeist;" - echo "CREATE USER 'flaschengeist'@'localhost' IDENTIFIED BY 'flaschengeist';" - echo "GRANT ALL PRIVILEGES ON flaschengeist.* TO 'flaschengeist'@'localhost';" - echo "FLUSH PRIVILEGES;" - ) | sudo mysql - -Then you can install the database tables and initial entries: - - $ flaschengeist install - ### Run Flaschengeist provides a CLI, based on the flask CLI, respectivly called `flaschengeist`. diff --git a/flaschengeist/database.py b/flaschengeist/database.py index f264715..560f7ed 100644 --- a/flaschengeist/database.py +++ b/flaschengeist/database.py @@ -22,6 +22,9 @@ migrate = Migrate() @migrate.configure def configure_alembic(config): + """Alembic configuration hook + + """ # Load migration paths from plugins migrations = [str(p.migrations_path) for p in current_app.config["FG_PLUGINS"].values() if p and p.migrations_path] if len(migrations) > 0: diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 228c0d6..be73cac 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -1,3 +1,29 @@ +"""Flaschengeist Plugins + +## Custom database tables + +You can add tables by declaring them using the SQLAlchemy syntax, +then use Alembic to generate migrations for your tables. +This allows Flaschengeist to proper up- or downgrade the +database tables if an user updates your plugin. + +migrations have to be provided in a directory called `migrations` +next to your plugin. E.G. + + myplugin + - __init__.py + - other/ + - ... + - migrations/ + +## Useful Hooks +There are some predefined hooks, which might get handy for you. + +For more information, please refer to +- `flaschengeist.utils.hook.HookBefore` and +- `flaschengeist.utils.hook.HookAfter` +""" + from importlib_metadata import Distribution, EntryPoint from werkzeug.datastructures import FileStorage from werkzeug.exceptions import MethodNotAllowed, NotFound @@ -5,49 +31,73 @@ from werkzeug.exceptions import MethodNotAllowed, NotFound from flaschengeist.models.user import _Avatar, User from flaschengeist.utils.hook import HookBefore, HookAfter +__all__ = [ + "plugins_installed", + "plugins_loaded", + "before_delete_user", + "before_role_updated", + "before_update_user", + "after_role_updated", + "Plugin", + "AuthPlugin", +] + +# Documentation hacks, see https://github.com/mitmproxy/pdoc/issues/320 plugins_installed = HookAfter("plugins.installed") -"""Hook decorator for when all plugins are installed - Possible use case would be to populate the database with some presets. +plugins_installed.__doc__ = """Hook decorator for when all plugins are installed - Args: - hook_result: void (kwargs) +Possible use case would be to populate the database with some presets. """ + plugins_loaded = HookAfter("plugins.loaded") -"""Hook decorator for when all plugins are loaded - Possible use case would be to check if a specific other plugin is loaded and change own behavior +plugins_loaded.__doc__ = """Hook decorator for when all plugins are loaded - Args: - app: Current flask app instance (args) - hook_result: void (kwargs) +Possible use case would be to check if a specific other plugin is loaded and change own behavior + +Passed args: + - *app:* Current flask app instance (args) """ + before_role_updated = HookBefore("update_role") -"""Hook decorator for when roles are modified -Args: - role: Role object to modify - new_name: New name if the name was changed (None if delete) +before_role_updated.__doc__ = """Hook decorator for when roles are modified + +Passed args: + - *role:* `flaschengeist.models.user.Role` to modify + - *new_name:* New name if the name was changed (*None* if delete) """ + after_role_updated = HookAfter("update_role") -"""Hook decorator for when roles are modified -Args: - role: Role object containing the modified role - new_name: New name if the name was changed (None if deleted) +after_role_updated.__doc__ = """Hook decorator for when roles are modified + +Passed args: + - *role:* modified `flaschengeist.models.user.Role` + - *new_name:* New name if the name was changed (*None* if deleted) """ + before_update_user = HookBefore("update_user") -"""Hook decorator, when ever an user update is done, this is called before. -Args: - user: User object +before_update_user.__doc__ = """Hook decorator, when ever an user update is done, this is called before. + +Passed args: + - *user:* `flaschengeist.models.user.User` object """ + before_delete_user = HookBefore("delete_user") -"""Hook decorator,this is called before an user gets deleted. -Args: - user: User object +before_delete_user.__doc__ = """Hook decorator,this is called before an user gets deleted. + +Passed args: + - *user:* `flaschengeist.models.user.User` object """ class Plugin: """Base class for all Plugins - If your class uses custom models add a static property called ``models``. + All plugins must derived from this class. + + Optional: + - *blueprint*: `flask.Blueprint` providing your routes + - *permissions*: List of your custom permissions + - *models*: Your models, used for API export """ name: str @@ -219,14 +269,14 @@ class AuthPlugin(Plugin): """ raise NotFound - def set_avatar(self, user: User, file: FileStorage): + def set_avatar(self, user, file: FileStorage): """Set the avatar for given user (if supported by auth backend) Default behavior is to use native Image objects stored on the Flaschengeist server Args: user: User to set the avatar for - file: FileStorage object uploaded by the user + file: `werkzeug.datastructures.FileStorage` uploaded by the user Raises: MethodNotAllowed: If not supported by Backend Any valid HTTP exception diff --git a/flaschengeist/plugins/balance/routes.py b/flaschengeist/plugins/balance/routes.py index f62a065..f0edc62 100644 --- a/flaschengeist/plugins/balance/routes.py +++ b/flaschengeist/plugins/balance/routes.py @@ -134,7 +134,7 @@ def get_balance(userid, current_session: Session): Route: ``/users//balance`` | Method: ``GET`` - GET-parameters: ```{from?: string, to?: string}``` + GET-parameters: ``{from?: string, to?: string}`` Args: userid: Userid of user to get balance from @@ -173,7 +173,7 @@ def get_transactions(userid, current_session: Session): Route: ``/users//balance/transactions`` | Method: ``GET`` - GET-parameters: ```{from?: string, to?: string, limit?: int, offset?: int}``` + GET-parameters: ``{from?: string, to?: string, limit?: int, offset?: int}`` Args: userid: Userid of user to get transactions from diff --git a/flaschengeist/utils/hook.py b/flaschengeist/utils/hook.py index b028f30..f7c7fb7 100644 --- a/flaschengeist/utils/hook.py +++ b/flaschengeist/utils/hook.py @@ -7,6 +7,7 @@ _hooks_after = {} def Hook(function=None, id=None): """Hook decorator + Use to decorate functions as hooks, so plugins can hook up their custom functions. """ # `id` passed as `arg` not `kwarg` @@ -38,8 +39,10 @@ def Hook(function=None, id=None): def HookBefore(id: str): """Decorator for functions to be called before a Hook-Function is called + The hooked up function must accept the same arguments as the function hooked onto, as the functions are called with the same arguments. + Hint: This enables you to modify the arguments! """ if not id or not isinstance(id, str): @@ -54,9 +57,18 @@ def HookBefore(id: str): def HookAfter(id: str): """Decorator for functions to be called after a Hook-Function is called + As with the HookBefore, the hooked up function must accept the same arguments as the function hooked onto, but also receives a `hook_result` kwarg containing the result of the function. + + Example: + ```py + @HookAfter("some.id") + def my_func(hook_result): + # This function is executed after the function registered with "some.id" + print(hook_result) # This is the result of the function + ``` """ if not id or not isinstance(id, str):