Compare commits

..

12 Commits

Author SHA1 Message Date
Ferdinand Thiessen 8e18a11fc8 feat(cli): Added CLI command for handling plugins
continuous-integration/woodpecker the build failed Details
* Install / Uninstall plugins
* List plugins
2021-12-23 02:50:04 +01:00
Ferdinand Thiessen 878a61f1c2 feat(plugins): Identify plugins by id, migrations must be provided at defined location, add utils for plugin functions 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen c4bf33d1c7 fix(docs): Various documentation fixed and improvments 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen a1a20b0d65 docs(migrations): Some documentation ++ 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen c647c7c8f8 feat(docs): Add documentation on how to install tables 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen a9416e5ca3 fix(db): Remove print statement for debugging 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen fc58f8c952 feat(db): Add initial migrations for core Flaschengeist + balance and pricelist plugins 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen 92b231224d feat(db): Add migrations support to plugins 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen e4366b8e9e fix(db): Add __repr__ to custom column types, same as done by SQLAlchemy 2021-12-23 02:50:04 +01:00
Ferdinand Thiessen 095159af71 feat(db): Add database migration support, implements #19
Migrations allow us to keep track of database changes and upgrading databases if needed.
2021-12-23 02:50:04 +01:00
Ferdinand Thiessen a6cbc002f6 fix(cli): InterfaceGenerator now works even without a namespace defined
continuous-integration/woodpecker the build failed Details
2021-12-23 02:48:28 +01:00
Ferdinand Thiessen 34ee95c66a feat(cli): Split CLI commands into seperate files 2021-12-23 02:48:02 +01:00
6 changed files with 139 additions and 111 deletions

View File

@ -1,3 +1,4 @@
import io
import sys
import inspect
import logging
@ -93,15 +94,32 @@ class InterfaceGenerator:
self.basename = models.__name__
self.walker(("models", models))
def _write_types(self):
TYPE = "type {name} = {alias};\n"
INTERFACE = "interface {name} {{\n{properties}}}\n"
PROPERTY = "\t{name}{modifier}: {type};\n"
buffer = io.StringIO()
for cls, props in self.classes.items():
if isinstance(props, str):
buffer.write(TYPE.format(name=cls, alias=props))
else:
buffer.write(
INTERFACE.format(
name=cls,
properties="".join(
[PROPERTY.format(name=name, modifier=props[name][0], type=props[name][1]) for name in props]
),
)
)
return buffer
def write(self):
with (open(self.filename, "w") if self.filename else sys.stdout) as file:
file.write("declare namespace {} {{\n".format(self.namespace))
for cls, params in self.classes.items():
if isinstance(params, str):
file.write("\ttype {} = {};\n".format(cls, params))
else:
file.write("\tinterface {} {{\n".format(cls))
for name in params:
file.write("\t\t{}{}: {};\n".format(name, *params[name]))
file.write("\t}\n")
file.write("}\n")
if self.namespace:
file.write(f"declare namespace {self.namespace} {{\n")
for line in self._write_types().getvalue().split("\n"):
file.write(f"\t{line}\n")
file.write("}\n")
else:
file.write(self._write_types().getvalue())

View File

@ -1,10 +1,5 @@
import pathlib
import subprocess
import click
from os import environ
from flask import current_app
from flask.cli import FlaskGroup, run_command, with_appcontext
import pkg_resources
from flask.cli import FlaskGroup, with_appcontext
from flaschengeist.app import create_app
from flaschengeist.config import configure_logger
@ -63,98 +58,14 @@ def cli():
pass
@cli.command()
@with_appcontext
def install():
"""Install and initialize enabled plugins.
def main(*args, **kwargs):
from .plugin_cmd import plugin
from .export_cmd import export
from .docs_cmd import docs
from .run_cmd import run
Most plugins need to install custom tables into the database
running this command will lookup all enabled plugins and run
their database initalization routines.
"""
from flaschengeist.app import install_all
install_all()
@cli.command()
@click.option("--output", "-o", help="Output file, default is stdout", type=click.Path())
@click.option("--namespace", "-n", help="TS namespace for the interfaces", type=str, show_default=True)
@click.option("--plugin", "-p", help="Also export types for a plugin (even if disabled)", multiple=True, type=str)
@click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True)
def export(namespace, output, no_core, plugin):
from flaschengeist import models
from flaschengeist import logger
from .InterfaceGenerator import InterfaceGenerator
gen = InterfaceGenerator(namespace, output, logger)
if not no_core:
gen.run(models)
if plugin:
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugins"):
if len(plugin) == 0 or entry_point.name in plugin:
plg = entry_point.load()
if hasattr(plg, "models") and plg.models is not None:
gen.run(plg.models)
gen.write()
@cli.command()
@click.option("--host", help="set hostname to listen on", default="127.0.0.1", show_default=True)
@click.option("--port", help="set port to listen on", type=int, default=5000, show_default=True)
@click.option("--debug", help="run in debug mode", is_flag=True)
@with_appcontext
@click.pass_context
def run(ctx, host, port, debug):
"""Run Flaschengeist using a development server."""
class PrefixMiddleware(object):
def __init__(self, app, prefix=""):
self.app = app
self.prefix = prefix
def __call__(self, environ, start_response):
if environ["PATH_INFO"].startswith(self.prefix):
environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
environ["SCRIPT_NAME"] = self.prefix
return self.app(environ, start_response)
else:
start_response("404", [("Content-Type", "text/plain")])
return ["This url does not belong to the app.".encode()]
from flaschengeist.config import config
# re configure logger, as we are no logger in CLI mode
configure_logger()
current_app.wsgi_app = PrefixMiddleware(current_app.wsgi_app, prefix=config["FLASCHENGEIST"].get("root", ""))
if debug:
environ["FLASK_DEBUG"] = "1"
environ["FLASK_ENV"] = "development"
ctx.invoke(run_command, host=host, port=port, debugger=debug)
@cli.command()
@click.option(
"--output",
"-o",
help="Documentation output path",
default="./docs",
type=click.Path(file_okay=False, path_type=pathlib.Path),
)
def docs(output: pathlib.Path):
"""Generate and export API documentation using pdoc3"""
output.mkdir(parents=True, exist_ok=True)
command = [
"python",
"-m",
"pdoc",
"--skip-errors",
"--html",
"--output-dir",
str(output),
"flaschengeist",
]
click.echo(f"Running command: {command}")
subprocess.check_call(command)
cli.add_command(plugin)
cli.add_command(export)
cli.add_command(docs)
cli.add_command(run)
cli(*args, **kwargs)

View File

@ -0,0 +1,38 @@
import click
import pathlib
import subprocess
@click.command()
@click.option(
"--output",
"-o",
help="Documentation output path",
default="./docs",
type=click.Path(file_okay=False, path_type=pathlib.Path),
)
@click.pass_context
def docs(ctx: click.Context, output: pathlib.Path):
"""Generate and export API documentation using pdoc"""
import pkg_resources
try:
pkg_resources.get_distribution("pdoc>=8.0.1")
except pkg_resources.DistributionNotFound:
click.echo(
f"Error: pdoc was not found, maybe you need to install it. Try:\n" "\n" '$ pip install "pdoc>=8.0.1"\n'
)
ctx.exit(1)
output.mkdir(parents=True, exist_ok=True)
command = [
"python",
"-m",
"pdoc",
"--docformat",
"google",
"--output-directory",
str(output),
"flaschengeist",
]
click.echo(f"Running command: {command}")
subprocess.check_call(command)

View File

@ -0,0 +1,21 @@
import click
@click.command()
@click.option("--output", "-o", help="Output file, default is stdout", type=click.Path())
@click.option("--namespace", "-n", help="TS namespace for the interfaces", type=str, show_default=True)
@click.option("--plugin", "-p", help="Also export types for a plugin (even if disabled)", multiple=True, type=str)
@click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True)
def export(namespace, output, no_core, plugin):
from flaschengeist import logger, models
from flaschengeist.app import get_plugins
from .InterfaceGenerator import InterfaceGenerator
gen = InterfaceGenerator(namespace, output, logger)
if not no_core:
gen.run(models)
if plugin:
for plugin_class in get_plugins():
if (len(plugin) == 0 or plugin_class.id in plugin) and plugin_class.models is not None:
gen.run(plugin_class.models)
gen.write()

View File

@ -0,0 +1,40 @@
import click
from os import environ
from flask import current_app
from flask.cli import with_appcontext, run_command
class PrefixMiddleware(object):
def __init__(self, app, prefix=""):
self.app = app
self.prefix = prefix
def __call__(self, environ, start_response):
if environ["PATH_INFO"].startswith(self.prefix):
environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
environ["SCRIPT_NAME"] = self.prefix
return self.app(environ, start_response)
else:
start_response("404", [("Content-Type", "text/plain")])
return ["This url does not belong to the app.".encode()]
@click.command()
@click.option("--host", help="set hostname to listen on", default="127.0.0.1", show_default=True)
@click.option("--port", help="set port to listen on", type=int, default=5000, show_default=True)
@click.option("--debug", help="run in debug mode", is_flag=True)
@with_appcontext
@click.pass_context
def run(ctx, host, port, debug):
"""Run Flaschengeist using a development server."""
from flaschengeist.config import config, configure_logger
# re configure logger, as we are no logger in CLI mode
configure_logger()
current_app.wsgi_app = PrefixMiddleware(current_app.wsgi_app, prefix=config["FLASCHENGEIST"].get("root", ""))
if debug:
environ["FLASK_DEBUG"] = "1"
environ["FLASK_ENV"] = "development"
ctx.invoke(run_command, host=host, port=port, debugger=debug)

View File

@ -44,7 +44,7 @@ mysql =
[options.entry_points]
console_scripts =
flaschengeist = flaschengeist.cli:cli
flaschengeist = flaschengeist.cli:main
flask.commands =
ldap = flaschengeist.plugins.auth_ldap.cli:ldap
users = flaschengeist.plugins.users.cli:users