# coding=utf-8
# -*- coding: UTF-8 -*-
#
# Copyright 2015-2025, Vincenzo Arcidiacono;
# Licensed under the EUPL (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
r"""
Define the command line interface for web forms.
.. click:: schedula.utils.form.cli:cli
:prog: schedula form
:show-nested:
"""
import os
import click
import logging
import click_log
import os.path as osp
log = logging.getLogger('schedula.utils.form.cli')
[docs]
class _Logger(logging.Logger):
[docs]
def setLevel(self, level):
super(_Logger, self).setLevel(level)
frmt = "%(asctime)-15s:%(levelname)5.5s:%(name)s:%(message)s"
logging.basicConfig(level=level, format=frmt)
rlog = logging.getLogger()
# because `basicConfig()` does not reconfig root-logger when re-invoked.
rlog.level = level
logging.captureWarnings(True)
logger = _Logger('cli')
click_log.basic_config(logger)
@click.group(
'form', context_settings=dict(help_option_names=['-h', '--help'])
)
def cli():
"""
schedula forms command line tool.
"""
@cli.command(
'sample', short_help='Generates sample folder for a web form.'
)
@click.option(
'--folder', '-f', default='.', required=False,
help="Main folder where app and src are contained.",
type=click.Path(writable=True, file_okay=False)
)
@click_log.simple_verbosity_option(logger)
def sample(folder='.'):
"""
Writes a sample folder OUTPUT_FOLDER.
OUTPUT_FOLDER: schedula WebForm folder template. [default: .]
"""
import glob
import shutil
base_dir = osp.dirname(__file__)
sample_dir = osp.join(base_dir, 'sample')
root_dir = osp.join(folder, 'root')
for k in ('static', 'templates', 'translations'):
shutil.copytree(
osp.join(base_dir, k), osp.join(root_dir, k), dirs_exist_ok=True
)
for fp in glob.glob(osp.join(sample_dir, '*'), include_hidden=True):
if osp.relpath(fp, sample_dir) in ('package-lock.json', 'node_modules'):
continue
it = glob.glob(
osp.join(fp, '**', '*.*'), include_hidden=True, recursive=True
) if osp.isdir(fp) else [fp]
for f in it:
dst = osp.join(folder, osp.relpath(f, sample_dir))
if osp.isfile(f) and not osp.isfile(dst):
os.makedirs(osp.dirname(dst), exist_ok=True)
shutil.copy(f, dst)
@cli.command('build', short_help='Build main folder `src` files.')
@click.option(
'--folder', '-f', default='.', required=False,
help="Main folder where app and src are contained.",
type=click.Path(writable=True, file_okay=False)
)
@click_log.simple_verbosity_option(logger)
def build(folder='.'):
"""
Build main folder `src` files.
"""
import subprocess
subprocess.run('npm i; npm run build', cwd=folder, shell=True)
@cli.command(
'watch',
short_help='Run server in debug with continuous build of main folder `src` '
'files.',
context_settings=dict(
ignore_unknown_options=True, allow_extra_args=True,
)
)
@click.option(
'--folder', '-f', default='.', required=False,
help="Main folder where app and src are contained.",
type=click.Path(writable=True, file_okay=False)
)
@click.option(
'--app', '-a',
help="The Flask application or factory function to load, in the form "
"'module:name'.",
default='app:app',
required=False
)
@click.option(
'--only-flask',
help="The Flask application or factory function to load, in the form "
"'module:name'.",
is_flag=True,
required=False
)
@click_log.simple_verbosity_option(logger)
@click.pass_context
def watch(ctx, folder='.', app='app:app', only_flask=False):
"""
Run server in debug with continuous build of main folder `src` files.
MAIN_FOLDER: Folder path. [default: .]
"""
if only_flask:
from livereload import Server
from schedula.utils.form.gapp import get_module
module, code = app.split(':')
app = eval(code, get_module(module, (folder,)))
app.debug = True
options = {
ctx.args[i][2:].replace('-', '_'): ctx.args[i + 1]
for i in range(0, len(ctx.args), 2)
}
server = Server(app.wsgi_app)
server.watch(osp.join(folder, 'root', '**', '*.*'))
server.serve(**options)
else:
import subprocess
subprocess.run(
f'(npm i; npm run watch) & '
f'(python {__file__} watch --app {app} --only-flask '
f'{" ".join(ctx.args)})',
cwd=folder, shell=True
)
@cli.command('run', short_help='Run server.', context_settings=dict(
ignore_unknown_options=True, allow_extra_args=True,
))
@click.option(
'--folder', '-f', default='.', required=False,
help="Main folder where app and src are contained.",
type=click.Path(writable=True, file_okay=False)
)
@click.option(
'--app', '-a',
help="The Flask application or factory function to load, in the form "
"'module:name'.",
default='app:app',
required=False
)
@click_log.simple_verbosity_option(logger)
@click.pass_context
def run(ctx, folder='.', app='app:app'):
"""
Building of src files within MAIN_FOLDER.
MAIN_FOLDER: Folder path. [default: .]
"""
from .gapp import Application, get_module
module, code = app.split(':')
options = {
ctx.args[i][2:].replace('-', '_'): ctx.args[i + 1]
for i in range(0, len(ctx.args), 2)
}
if 'SECRET_KEY' not in os.environ:
import secrets
os.environ['SECRET_KEY'] = secrets.token_hex(32)
if 'SECURITY_PASSWORD_SALT' not in os.environ:
import secrets
salt = f'{secrets.SystemRandom().getrandbits(128)}'
os.environ['SECURITY_PASSWORD_SALT'] = salt
Application(
app=eval(code, get_module(module, (folder,))),
**options
).run()
if __name__ == '__main__':
cli()