feat(docs): Add documentation on how to install tables

Also various documentation fixed and improvments
This commit is contained in:
Ferdinand Thiessen 2021-12-20 00:53:49 +01:00
parent cfb4776a3c
commit 216b88c529
5 changed files with 139 additions and 60 deletions

View File

@ -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`.

View File

@ -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:

View File

@ -1,51 +1,109 @@
"""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 pkg_resources
from werkzeug.datastructures import FileStorage
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 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"""
@ -55,14 +113,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
@ -206,14 +268,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

View File

@ -131,7 +131,7 @@ def get_balance(userid, current_session: Session):
Route: ``/users/<userid>/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/<userid>/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

View File

@ -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):