feat(docs): Add documentation on how to install tables
Also various documentation fixed and improvments
This commit is contained in:
		
							parent
							
								
									5cf54c1a08
								
							
						
					
					
						commit
						f5c7e59162
					
				
							
								
								
									
										48
									
								
								README.md
								
								
								
								
							
							
						
						
									
										48
									
								
								README.md
								
								
								
								
							|  | @ -31,18 +31,35 @@ or if you want to also run the tests: | ||||||
| 
 | 
 | ||||||
|     pip3 install --user ".[ldap,tests]" |     pip3 install --user ".[ldap,tests]" | ||||||
| 
 | 
 | ||||||
| You will also need a MySQL driver, recommended drivers are | You will also need a MySQL driver, by default one of this is installed: | ||||||
| - `mysqlclient` | - `mysqlclient` (non Windows) | ||||||
| - `PyMySQL` | - `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 | ### Install database | ||||||
| Same as above, but if you want to use `mysqlclient` instead of `PyMySQL` (performance?) you have to follow this guide: | 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 | 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: | (where flaschegeist is installed) or create an empty one and place it inside either: | ||||||
| 1. `~/.config/` | 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) |   - 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 |   - 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 | ### Run | ||||||
| Flaschengeist provides a CLI, based on the flask CLI, respectivly called `flaschengeist`. | Flaschengeist provides a CLI, based on the flask CLI, respectivly called `flaschengeist`. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,9 +5,53 @@ | ||||||
|     - your_plugin/ |     - your_plugin/ | ||||||
|         - __init__.py |         - __init__.py | ||||||
|         - ... |         - ... | ||||||
|  |         - migrations/ (optional) | ||||||
|             - ... |             - ... | ||||||
|     - setup.cfg |     - setup.cfg | ||||||
| 
 | 
 | ||||||
| The basic layout of a plugin is quite simple, you will only need the `setup.cfg` or `setup.py` and | 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. | 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` | ||||||
|  |  | ||||||
|  | @ -22,6 +22,9 @@ migrate = Migrate() | ||||||
| 
 | 
 | ||||||
| @migrate.configure | @migrate.configure | ||||||
| def configure_alembic(config): | def configure_alembic(config): | ||||||
|  |     """Alembic configuration hook | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|     # Load migration paths from plugins |     # 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] |     migrations = [str(p.migrations_path) for p in current_app.config["FG_PLUGINS"].values() if p and p.migrations_path] | ||||||
|     if len(migrations) > 0: |     if len(migrations) > 0: | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
| .. include:: docs/plugin_development.md | .. include:: docs/plugin_development.md | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
|  | 
 | ||||||
| from importlib_metadata import Distribution, EntryPoint | from importlib_metadata import Distribution, EntryPoint | ||||||
| from werkzeug.datastructures import FileStorage | from werkzeug.datastructures import FileStorage | ||||||
| from werkzeug.exceptions import MethodNotAllowed, NotFound | from werkzeug.exceptions import MethodNotAllowed, NotFound | ||||||
|  | @ -10,49 +11,73 @@ from werkzeug.exceptions import MethodNotAllowed, NotFound | ||||||
| from flaschengeist.models.user import _Avatar, User | from flaschengeist.models.user import _Avatar, User | ||||||
| from flaschengeist.utils.hook import HookBefore, HookAfter | from flaschengeist.utils.hook import HookBefore, HookAfter | ||||||
| 
 | 
 | ||||||
| plugins_installed = HookAfter("plugins.installed") | __all__ = [ | ||||||
| """Hook decorator for when all plugins are installed |     "plugins_installed", | ||||||
|     Possible use case would be to populate the database with some presets. |     "plugins_loaded", | ||||||
|  |     "before_delete_user", | ||||||
|  |     "before_role_updated", | ||||||
|  |     "before_update_user", | ||||||
|  |     "after_role_updated", | ||||||
|  |     "Plugin", | ||||||
|  |     "AuthPlugin", | ||||||
|  | ] | ||||||
| 
 | 
 | ||||||
|     Args: | # Documentation hacks, see https://github.com/mitmproxy/pdoc/issues/320 | ||||||
|         hook_result: void (kwargs) | plugins_installed = HookAfter("plugins.installed") | ||||||
|  | plugins_installed.__doc__ = """Hook decorator for when all plugins are installed | ||||||
|  | 
 | ||||||
|  | Possible use case would be to populate the database with some presets. | ||||||
| """ | """ | ||||||
|  | 
 | ||||||
| plugins_loaded = HookAfter("plugins.loaded") | plugins_loaded = HookAfter("plugins.loaded") | ||||||
| """Hook decorator for when all plugins are loaded | plugins_loaded.__doc__ = """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 | Possible use case would be to check if a specific other plugin is loaded and change own behavior | ||||||
| 
 | 
 | ||||||
|     Args: | Passed args: | ||||||
|         app: Current flask app instance (args) |     - *app:* Current flask app instance (args) | ||||||
|         hook_result: void (kwargs) |  | ||||||
| """ | """ | ||||||
|  | 
 | ||||||
| before_role_updated = HookBefore("update_role") | before_role_updated = HookBefore("update_role") | ||||||
| """Hook decorator for when roles are modified | before_role_updated.__doc__ = """Hook decorator for when roles are modified | ||||||
| Args: |      | ||||||
|     role: Role object to modify | Passed args: | ||||||
|     new_name: New name if the name was changed (None if delete) |     - *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") | after_role_updated = HookAfter("update_role") | ||||||
| """Hook decorator for when roles are modified | after_role_updated.__doc__ = """Hook decorator for when roles are modified | ||||||
| Args: | 
 | ||||||
|     role: Role object containing the modified role | Passed args: | ||||||
|     new_name: New name if the name was changed (None if deleted) |     - *role:* modified `flaschengeist.models.user.Role` | ||||||
|  |     - *new_name:* New name if the name was changed (*None* if deleted) | ||||||
| """ | """ | ||||||
|  | 
 | ||||||
| before_update_user = HookBefore("update_user") | before_update_user = HookBefore("update_user") | ||||||
| """Hook decorator, when ever an user update is done, this is called before. | before_update_user.__doc__ = """Hook decorator, when ever an user update is done, this is called before. | ||||||
| Args: | 
 | ||||||
|     user: User object | Passed args: | ||||||
|  |     - *user:* `flaschengeist.models.user.User` object | ||||||
| """ | """ | ||||||
|  | 
 | ||||||
| before_delete_user = HookBefore("delete_user") | before_delete_user = HookBefore("delete_user") | ||||||
| """Hook decorator,this is called before an user gets deleted. | before_delete_user.__doc__ = """Hook decorator,this is called before an user gets deleted. | ||||||
| Args: | 
 | ||||||
|     user: User object | Passed args: | ||||||
|  |     - *user:* `flaschengeist.models.user.User` object | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Plugin: | class Plugin: | ||||||
|     """Base class for all Plugins |     """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 |     name: str | ||||||
|  | @ -224,14 +249,14 @@ class AuthPlugin(Plugin): | ||||||
|         """ |         """ | ||||||
|         raise NotFound |         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) |         """Set the avatar for given user (if supported by auth backend) | ||||||
| 
 | 
 | ||||||
|         Default behavior is to use native Image objects stored on the Flaschengeist server |         Default behavior is to use native Image objects stored on the Flaschengeist server | ||||||
| 
 | 
 | ||||||
|         Args: |         Args: | ||||||
|             user: User to set the avatar for |             user: User to set the avatar for | ||||||
|             file: FileStorage object uploaded by the user |             file: `werkzeug.datastructures.FileStorage` uploaded by the user | ||||||
|         Raises: |         Raises: | ||||||
|             MethodNotAllowed: If not supported by Backend |             MethodNotAllowed: If not supported by Backend | ||||||
|             Any valid HTTP exception |             Any valid HTTP exception | ||||||
|  |  | ||||||
|  | @ -134,7 +134,7 @@ def get_balance(userid, current_session: Session): | ||||||
| 
 | 
 | ||||||
|     Route: ``/users/<userid>/balance`` | Method: ``GET`` |     Route: ``/users/<userid>/balance`` | Method: ``GET`` | ||||||
| 
 | 
 | ||||||
|     GET-parameters: ```{from?: string, to?: string}``` |     GET-parameters: ``{from?: string, to?: string}`` | ||||||
| 
 | 
 | ||||||
|     Args: |     Args: | ||||||
|         userid: Userid of user to get balance from |         userid: Userid of user to get balance from | ||||||
|  | @ -173,7 +173,7 @@ def get_transactions(userid, current_session: Session): | ||||||
| 
 | 
 | ||||||
|     Route: ``/users/<userid>/balance/transactions`` | Method: ``GET`` |     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: |     Args: | ||||||
|         userid: Userid of user to get transactions from |         userid: Userid of user to get transactions from | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ _hooks_after = {} | ||||||
| 
 | 
 | ||||||
| def Hook(function=None, id=None): | def Hook(function=None, id=None): | ||||||
|     """Hook decorator |     """Hook decorator | ||||||
|  | 
 | ||||||
|     Use to decorate functions as hooks, so plugins can hook up their custom functions. |     Use to decorate functions as hooks, so plugins can hook up their custom functions. | ||||||
|     """ |     """ | ||||||
|     # `id` passed as `arg` not `kwarg` |     # `id` passed as `arg` not `kwarg` | ||||||
|  | @ -38,8 +39,10 @@ def Hook(function=None, id=None): | ||||||
| 
 | 
 | ||||||
| def HookBefore(id: str): | def HookBefore(id: str): | ||||||
|     """Decorator for functions to be called before a Hook-Function is called |     """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, |     The hooked up function must accept the same arguments as the function hooked onto, | ||||||
|     as the functions are called with the same arguments. |     as the functions are called with the same arguments. | ||||||
|  | 
 | ||||||
|     Hint: This enables you to modify the arguments! |     Hint: This enables you to modify the arguments! | ||||||
|     """ |     """ | ||||||
|     if not id or not isinstance(id, str): |     if not id or not isinstance(id, str): | ||||||
|  | @ -54,9 +57,18 @@ def HookBefore(id: str): | ||||||
| 
 | 
 | ||||||
| def HookAfter(id: str): | def HookAfter(id: str): | ||||||
|     """Decorator for functions to be called after a Hook-Function is called |     """Decorator for functions to be called after a Hook-Function is called | ||||||
|  | 
 | ||||||
|     As with the HookBefore, the hooked up function must accept the same |     As with the HookBefore, the hooked up function must accept the same | ||||||
|     arguments as the function hooked onto, but also receives a |     arguments as the function hooked onto, but also receives a | ||||||
|     `hook_result` kwarg containing the result of the function. |     `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): |     if not id or not isinstance(id, str): | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue