From 0db6b6e72fec44275ebc40bd9ed9d67f0ebe7326 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 20 Dec 2021 00:53:49 +0100 Subject: [PATCH] feat(docs): Add documentation on how to install tables Also various documentation fixed and improvments --- README.md | 48 +++++++-------- docs/plugin_development.md | 44 ++++++++++++++ flaschengeist/plugins/__init__.py | 77 ++++++++++++++++--------- flaschengeist/plugins/balance/routes.py | 4 +- flaschengeist/utils/hook.py | 12 ++++ 5 files changed, 134 insertions(+), 51 deletions(-) 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/docs/plugin_development.md b/docs/plugin_development.md index 21b69c5..ee129eb 100644 --- a/docs/plugin_development.md +++ b/docs/plugin_development.md @@ -5,9 +5,53 @@ - your_plugin/ - __init__.py - ... + - migrations/ (optional) - ... - setup.cfg The basic layout of a plugin is quite simple, you will only need the `setup.cfg` or `setup.py` and the package containing your plugin code, at lease a `__init__.py` file with your `Plugin` class. +If you use custom database tables you need to provide a `migrations` directory within your package, +see next section. + +## Database Tables / Migrations +To allow upgrades of installed plugins, the database is versioned and handled +through [Alembic](https://alembic.sqlalchemy.org/en/latest/index.html) migrations. +Each plugin, which uses custom database tables, is represented as an other base. +So you could simply follow the Alembic tutorial on [how to work with multiple bases](https://alembic.sqlalchemy.org/en/latest/branches.html#creating-a-labeled-base-revision). + +A quick overview on how to work with migrations for your plugin: + + $ flaschengeist db revision -m "Create my super plugin" \ + --head=base --branch-label=myplugin_name --version-path=your/plugin/migrations + +This would add a new base named `myplugin_name`, which should be the same as the pypi name of you plugin. +If your tables depend on an other plugin or a specific base version you could of cause add + + --depends-on=VERSION + +or + + --depends-on=other_plugin + + +### Plugin Removal and Database Tables +As generic downgrades are most often hard to write, your plugin is not required to provide such functionallity. +For Flaschengeist only instable versions provide meaningful downgrade migrations down to the latest stable version. + +So this means if you do not provide downgrades you must at lease provide a series of migrations toward removal of +the database tables in case the users wants to delete the plugin. + + (base) ----> 1.0 <----> 1.1 <----> 1.2 + | + --> removal + +After the removal step the database is stamped to to "remove" your + +## 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` diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 7a01939..edfe243 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -3,6 +3,7 @@ .. include:: docs/plugin_development.md """ + from importlib_metadata import Distribution, EntryPoint from werkzeug.exceptions import MethodNotAllowed, NotFound from werkzeug.datastructures import FileStorage @@ -10,49 +11,73 @@ from werkzeug.datastructures import FileStorage 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 @@ -240,14 +265,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):