From 5669220b5d074b3349d70d3e6b8bed2e291c9fc7 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 23 Dec 2021 02:42:00 +0100 Subject: [PATCH] fix(docs): Various documentation fixed and improvments --- README.md | 3 +- flaschengeist/database.py | 3 + flaschengeist/plugins/__init__.py | 131 ++++++++++++++++++------ flaschengeist/plugins/balance/routes.py | 4 +- flaschengeist/utils/hook.py | 12 +++ 5 files changed, 116 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 7898906..b63c5f0 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,12 @@ If not you need to create user and database manually do (or similar on Windows): ) | sudo mysql 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: +To only upgrade one plugin (for example the `events` plugin): $ flaschengeist db upgrade events@head 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 bcd96b3..d993879 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` +""" + import sqlalchemy import pkg_resources from werkzeug.datastructures import FileStorage @@ -10,48 +36,81 @@ from flaschengeist.models.user import _Avatar, User from flaschengeist.models.setting import _PluginSetting 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 be derived from this class. + There are some static properties a plugin must provide, + and some properties a plugin can provide if you might want + to use more functionality. + + Required: + - *id*: Unique identifier of your plugin + + Optional: + - *blueprint*: `flask.Blueprint` providing your routes + - *permissions*: List of your custom permissions + - *models*: Your models, used for API export + - *version*: Version of your plugin, can also be guessed by Flaschengeist + """ blueprint = None # You have to override """Override with a `flask.blueprint` if the plugin uses custom routes""" @@ -61,14 +120,18 @@ class Plugin: A good style is to name the permissions with a prefix related to the plugin name, to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*. """ - id = "dev.flaschengeist.plugin" # You have to override - """Override with the unique ID of the plugin (Hint: FQN)""" - name = "plugin" # You have to override - """Override with human readable name of the plugin""" - models = None # You have to override - """Override with models module""" - migrations_path = None # Override this with the location of your db migrations directory - """Override with path to migration files, if custome db tables are used""" + + models = None + """Override with models module + + Used for API export, has to be a static property + """ + + version = None + """Override with a custom version, optional + + If not set, the version is guessed from the package / distribution + """ def __init__(self, config=None): """Constructor called by create_app @@ -233,14 +296,14 @@ class AuthPlugin(Plugin): """ raise NotFound - def set_avatar(self, user: User, file: FileStorage): + def set_avatar(self, user, file): """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 b135c52..4f19ead 100644 --- a/flaschengeist/plugins/balance/routes.py +++ b/flaschengeist/plugins/balance/routes.py @@ -131,7 +131,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 @@ -170,7 +170,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):