2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-13 14:58:47 +00:00

Invoke verbosity (#11706)

* Reduce verbosity of invoke tasks

- Suppress some django messages which are not useful to most users
- Verbosity can be added with --verbose flag

* Further improvements

* Better messaging

* Extra options

* No!
This commit is contained in:
Oliver
2026-04-10 07:58:53 +10:00
committed by GitHub
parent 8d24abcb2a
commit 4b3b03ed4b
3 changed files with 170 additions and 72 deletions

View File

@@ -13,25 +13,39 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
"""Wait till the database is ready.""" """Wait till the database is ready."""
self.stdout.write('Waiting for database...')
connected = False connected = False
verbose = int(kwargs.get('verbosity', 0)) > 0
attempts = kwargs.get('attempts', 10)
while not connected: if verbose:
time.sleep(2) self.stdout.write('Waiting for database connection...')
self.stdout.flush()
while not connected and attempts > 0:
attempts -= 1
try: try:
connection.ensure_connection() connection.ensure_connection()
connected = True connected = True
except OperationalError as e: except (OperationalError, ImproperlyConfigured):
self.stdout.write(f'Could not connect to database: {e}') if verbose:
except ImproperlyConfigured as e: self.stdout.write('Database connection failed, retrying ...')
self.stdout.write(f'Improperly configured: {e}') self.stdout.flush()
else: else:
if not connection.is_usable(): if not connection.is_usable():
self.stdout.write('Database configuration is not usable') if verbose:
self.stdout.write('Database configuration is not usable')
self.stdout.flush()
if connected: if connected:
self.stdout.write('Database connection successful!') if verbose:
self.stdout.write('Database connection successful!')
self.stdout.flush()
else:
time.sleep(1)
if not connected:
self.stderr.write('Failed to connect to database after multiple attempts')
self.stderr.flush()

View File

@@ -13,7 +13,7 @@ import { __INVENTREE_VERSION_INFO__ } from './version-info';
const IS_IN_WSL = platform().includes('WSL') || release().includes('WSL'); const IS_IN_WSL = platform().includes('WSL') || release().includes('WSL');
if (IS_IN_WSL) { if (IS_IN_WSL) {
console.log('WSL detected: using polling for file system events'); console.debug('WSL detected: using polling for file system events');
} }
// Output directory for the built files // Output directory for the built files

206
tasks.py
View File

@@ -9,6 +9,7 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from platform import python_version from platform import python_version
@@ -200,9 +201,13 @@ def state_logger(fn=None, method_name=None):
do_log = is_debug_environment() do_log = is_debug_environment()
if do_log: if do_log:
info(f'# task | {func.method_name} | start') info(f'# task | {func.method_name} | start')
t1 = time.time()
func(c, *args, **kwargs) func(c, *args, **kwargs)
t2 = time.time()
if do_log: if do_log:
info(f'# task | {func.method_name} | done') info(f'# task | {func.method_name} | done | elapsed: {t2 - t1:.2f}s')
return wrapped return wrapped
@@ -464,7 +469,7 @@ def run(
return result return result
def manage(c, cmd, pty: bool = False, env=None, **kwargs): def manage(c, cmd, pty: bool = False, env=None, verbose: bool = False, **kwargs):
"""Runs a given command against django's "manage.py" script. """Runs a given command against django's "manage.py" script.
Args: Args:
@@ -472,7 +477,14 @@ def manage(c, cmd, pty: bool = False, env=None, **kwargs):
cmd: Django command to run. cmd: Django command to run.
pty (bool, optional): Run an interactive session. Defaults to False. pty (bool, optional): Run an interactive session. Defaults to False.
env (dict, optional): Environment variables to pass to the command. Defaults to None. env (dict, optional): Environment variables to pass to the command. Defaults to None.
verbose (bool, optional): Print verbose output from the command. Defaults to False.
""" """
if verbose:
info(f'Running command: python3 manage.py {cmd}')
cmd += ' -v 1'
else:
cmd += ' -v 0'
return run( return run(
c, f'python3 manage.py {cmd}', manage_py_dir(), pty=pty, env=env, **kwargs c, f'python3 manage.py {cmd}', manage_py_dir(), pty=pty, env=env, **kwargs
) )
@@ -499,8 +511,19 @@ def run_install(
run_preflight=True, run_preflight=True,
version_check=False, version_check=False,
pinned=True, pinned=True,
verbose: bool = False,
): ):
"""Run the installation of python packages from a requirements file.""" """Run the installation of python packages from a requirements file.
Arguments:
c: Command line context.
uv: Whether to use UV (experimental package manager) instead of pip.
install_file: Path to the requirements file to install from.
run_preflight: Whether to run the preflight installation step (installing pip/uv itself). Default is True.
version_check: Whether to check for a version-specific requirements file. Default is False.
pinned: Whether to use the --require-hashes option when installing packages. Default is True.
verbose: Whether to print verbose output from pip install commands. Default is False.
"""
if version_check: if version_check:
# Test if there is a version specific requirements file # Test if there is a version specific requirements file
sys_ver_s = python_version().split('.') sys_ver_s = python_version().split('.')
@@ -518,25 +541,28 @@ def run_install(
# Install required Python packages with PIP # Install required Python packages with PIP
if not uv: if not uv:
# Optionally run preflight first
if run_preflight: if run_preflight:
run( run(
c, c,
'pip3 install --no-cache-dir --disable-pip-version-check -U pip setuptools', f'pip3 install --no-cache-dir --disable-pip-version-check -U pip setuptools {"" if verbose else "--quiet"}',
) )
info('Installed package manager')
run( run(
c, c,
f'pip3 install --no-cache-dir --disable-pip-version-check -U {"--require-hashes" if pinned else ""} -r {install_file}', f'pip3 install --no-cache-dir --disable-pip-version-check -U {"--require-hashes" if pinned else ""} -r {install_file} {"" if verbose else "--quiet"}',
) )
else: else:
if run_preflight: if run_preflight:
run( run(
c, c,
'pip3 install --no-cache-dir --disable-pip-version-check -U uv setuptools', f'pip3 install --no-cache-dir --disable-pip-version-check -U uv setuptools {"" if verbose else "--quiet"}',
) )
info('Installed package manager') info('Installed package manager')
run( run(
c, c,
f'uv pip install -U {"--require-hashes" if pinned else ""} -r {install_file}', f'uv pip install -U {"--require-hashes" if pinned else ""} -r {install_file} {"" if verbose else "--quiet"}',
) )
@@ -605,28 +631,34 @@ def check_file_existence(filename: Path, overwrite: bool = False):
sys.exit(1) sys.exit(1)
@task @task(help={'verbose': 'Print verbose output from the command'})
@state_logger @state_logger
def wait(c): def wait(c, verbose: bool = False):
"""Wait until the database connection is ready.""" """Wait until the database connection is ready."""
info('Waiting for database connection...') return manage(c, 'wait_for_db', verbose=verbose)
return manage(c, 'wait_for_db')
# Install tasks # Install tasks
# region tasks # region tasks
@task(help={'uv': 'Use UV (experimental package manager)'}) @task(
help={
'uv': 'Use UV (experimental package manager)',
'verbose': 'Print verbose output from installation commands',
}
)
@state_logger @state_logger
def plugins(c, uv=False): def plugins(c, uv: bool = False, verbose: bool = False):
"""Installs all plugins as specified in 'plugins.txt'.""" """Installs all plugins as specified in 'plugins.txt'."""
from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import]
get_plugin_file, get_plugin_file,
) )
run_install(c, uv, get_plugin_file(), run_preflight=False, pinned=False) run_install(
c, uv, get_plugin_file(), run_preflight=False, pinned=False, verbose=verbose
)
# Collect plugin static files # Collect plugin static files
manage(c, 'collectplugins') manage(c, 'collectplugins', verbose=verbose)
@task( @task(
@@ -634,29 +666,51 @@ def plugins(c, uv=False):
'uv': 'Use UV package manager (experimental)', 'uv': 'Use UV package manager (experimental)',
'skip_plugins': 'Skip plugin installation', 'skip_plugins': 'Skip plugin installation',
'dev': 'Install development requirements instead of production requirements', 'dev': 'Install development requirements instead of production requirements',
'verbose': 'Print verbose output from pip install commands',
} }
) )
@state_logger @state_logger
def install(c, uv=False, skip_plugins=False, dev=False): def install(
"""Installs required python packages.""" c,
uv: bool = False,
skip_plugins: bool = False,
dev: bool = False,
verbose: bool = False,
):
"""Install required python packages for InvenTree.
Arguments:
c: Command line context.
uv: Use UV package manager (experimental) instead of pip. Default is False.
skip_plugins: Skip plugin installation. Default is False.
dev: Install development requirements instead of production requirements. Default is False.
verbose: Print verbose output from pip install commands. Default is False.
"""
info('Installing required python packages...')
if dev: if dev:
run_install( run_install(
c, c,
uv, uv,
local_dir().joinpath('src/backend/requirements-dev.txt'), local_dir().joinpath('src/backend/requirements-dev.txt'),
version_check=True, version_check=True,
verbose=verbose,
) )
success('Dependency installation complete') success('Dependency installation complete')
return return
# Ensure path is relative to *this* directory # Ensure path is relative to *this* directory
run_install( run_install(
c, uv, local_dir().joinpath('src/backend/requirements.txt'), version_check=True c,
uv,
local_dir().joinpath('src/backend/requirements.txt'),
version_check=True,
verbose=verbose,
) )
# Run plugins install # Run plugins install
if not skip_plugins: if not skip_plugins:
plugins(c, uv=uv) plugins(c, uv=uv, verbose=verbose)
# Compile license information # Compile license information
lic_path = manage_py_dir().joinpath('InvenTree', 'licenses.txt') lic_path = manage_py_dir().joinpath('InvenTree', 'licenses.txt')
@@ -668,24 +722,26 @@ def install(c, uv=False, skip_plugins=False, dev=False):
success('Dependency installation complete') success('Dependency installation complete')
@task(help={'tests': 'Set up test dataset at the end'}) @task(
def setup_dev(c, tests=False): help={
'tests': 'Set up test dataset at the end',
'verbose': 'Print verbose output from commands',
}
)
def setup_dev(c, tests: bool = False, verbose: bool = False):
"""Sets up everything needed for the dev environment.""" """Sets up everything needed for the dev environment."""
# Install required Python packages with PIP # Install required Python packages with PIP
install(c, uv=False, skip_plugins=True, dev=True) install(c, uv=False, skip_plugins=True, dev=True, verbose=verbose)
# Install pre-commit hook # Install pre-commit hook
info('Installing pre-commit for checks before git commits...') info('Installing pre-commit for checks before git commits...')
run(c, 'pre-commit install') run(c, 'pre-commit install')
# Update all the hooks
run(c, 'pre-commit autoupdate') run(c, 'pre-commit autoupdate')
success('pre-commit set up complete') success('pre-commit set up complete')
# Set up test-data if flag is set # Set up test-data if flag is set
if tests: if tests:
setup_test(c) setup_test(c, verbose=verbose)
# Setup / maintenance tasks # Setup / maintenance tasks
@@ -725,7 +781,7 @@ def rebuild_thumbnails(c):
@state_logger @state_logger
def clean_settings(c): def clean_settings(c):
"""Clean the setting tables of old settings.""" """Clean the setting tables of old settings."""
info('Cleaning old settings from the database') info('Cleaning old settings from the database...')
manage(c, 'clean_settings') manage(c, 'clean_settings')
success('Settings cleaned successfully') success('Settings cleaned successfully')
@@ -797,12 +853,12 @@ def translate(c, ignore_static=False, no_frontend=False):
success('Translation files built successfully') success('Translation files built successfully')
@task @task(help={'verbose': 'Print verbose output'})
@state_logger('backend_trans') @state_logger('backend_trans')
def backend_trans(c): def backend_trans(c, verbose: bool = False):
"""Compile backend Django translation files.""" """Compile backend Django translation files."""
info('Compiling backend translations') info('Compiling backend translations...')
manage(c, 'compilemessages') manage(c, 'compilemessages', verbose=verbose)
success('Backend translations compiled successfully') success('Backend translations compiled successfully')
@@ -944,20 +1000,33 @@ def listbackups(c):
manage(c, 'listbackups') manage(c, 'listbackups')
@task(pre=[wait], post=[rebuild_models, rebuild_thumbnails]) @task(
pre=[wait],
post=[rebuild_models, rebuild_thumbnails],
help={
'verbose': 'Print verbose output from migration commands',
'detect': 'Detect and create new migrations based on changes to models',
},
)
@state_logger @state_logger
def migrate(c): def migrate(c, detect: bool = True, verbose: bool = False):
"""Performs database migrations. """Performs database migrations.
This is a critical step if the database schema have been altered! This is a critical step if the database schema have been altered!
""" """
info('Running InvenTree database migrations...') info('Running InvenTree database migrations...')
# Run custom management command which wraps migrations in "maintenance mode" if detect:
manage(c, 'makemigrations') manage(c, 'makemigrations', verbose=verbose)
manage(c, 'runmigrations', pty=True)
manage(c, 'migrate --run-syncdb') manage(c, 'runmigrations', pty=True, verbose=verbose)
manage(c, 'remove_stale_contenttypes --include-stale-apps --no-input', pty=True) manage(c, 'migrate --run-syncdb', verbose=verbose)
manage(
c,
'remove_stale_contenttypes --include-stale-apps --no-input',
pty=True,
verbose=verbose,
)
success('InvenTree database migrations completed') success('InvenTree database migrations completed')
@@ -978,6 +1047,7 @@ def showmigrations(c, app=''):
'no_frontend': 'Skip frontend compilation/download step', 'no_frontend': 'Skip frontend compilation/download step',
'skip_static': 'Skip static file collection step', 'skip_static': 'Skip static file collection step',
'uv': 'Use UV (experimental package manager)', 'uv': 'Use UV (experimental package manager)',
'verbose': 'Print verbose output from installation commands',
}, },
) )
@state_logger @state_logger
@@ -990,6 +1060,7 @@ def update(
no_frontend: bool = False, no_frontend: bool = False,
skip_static: bool = False, skip_static: bool = False,
uv: bool = False, uv: bool = False,
verbose: bool = False,
): ):
"""Update InvenTree installation. """Update InvenTree installation.
@@ -1009,7 +1080,7 @@ def update(
info('Updating InvenTree installation...') info('Updating InvenTree installation...')
# Ensure required components are installed # Ensure required components are installed
install(c, uv=uv) install(c, uv=uv, verbose=verbose)
# Skip backend translation compilation on docker, unless explicitly requested. # Skip backend translation compilation on docker, unless explicitly requested.
# Users can also forcefully disable the step via `--no-backend`. # Users can also forcefully disable the step via `--no-backend`.
@@ -1019,7 +1090,7 @@ def update(
else: else:
info('Skipping backend translation compilation (INVENTREE_DOCKER flag set)') info('Skipping backend translation compilation (INVENTREE_DOCKER flag set)')
else: else:
backend_trans(c) backend_trans(c, verbose=verbose)
if not skip_backup: if not skip_backup:
backup(c) backup(c)
@@ -1052,7 +1123,7 @@ def update(
# Note: frontend has already been compiled if required # Note: frontend has already been compiled if required
static(c, frontend=False) static(c, frontend=False)
success('InvenTree update complete!') success('InvenTree update complete')
# Data tasks # Data tasks
@@ -1066,8 +1137,8 @@ def update(
'exclude_plugins': 'Exclude plugin data from the output file (default = False)', 'exclude_plugins': 'Exclude plugin data from the output file (default = False)',
'include_sso': 'Include SSO token data in the output file (default = False)', 'include_sso': 'Include SSO token data in the output file (default = False)',
'include_session': 'Include user session data in the output file (default = False)', 'include_session': 'Include user session data in the output file (default = False)',
}, 'verbose': 'Print verbose output from management commands',
pre=[wait], }
) )
def export_records( def export_records(
c, c,
@@ -1079,6 +1150,7 @@ def export_records(
exclude_plugins: bool = False, exclude_plugins: bool = False,
include_sso: bool = False, include_sso: bool = False,
include_session: bool = False, include_session: bool = False,
verbose: bool = False,
): ):
"""Export all database records to a file.""" """Export all database records to a file."""
# Get an absolute path to the file # Get an absolute path to the file
@@ -1088,6 +1160,8 @@ def export_records(
info(f"Exporting database records to file '{target}'") info(f"Exporting database records to file '{target}'")
wait(c, verbose=verbose)
check_file_existence(target, overwrite) check_file_existence(target, overwrite)
excludes = content_excludes( excludes = content_excludes(
@@ -1104,8 +1178,7 @@ def export_records(
cmd = f"dumpdata --natural-foreign --indent 2 --output '{tmpfile.name}' {excludes}" cmd = f"dumpdata --natural-foreign --indent 2 --output '{tmpfile.name}' {excludes}"
# Dump data to temporary file # Dump data to temporary file
manage(c, cmd, pty=True) manage(c, cmd, pty=True, verbose=verbose)
info('Running data post-processing step...') info('Running data post-processing step...')
# Post-process the file, to remove any "permissions" specified for a user or group # Post-process the file, to remove any "permissions" specified for a user or group
@@ -1217,6 +1290,7 @@ def validate_import_metadata(
'ignore_nonexistent': 'Ignore non-existent database models (default = False)', 'ignore_nonexistent': 'Ignore non-existent database models (default = False)',
'exclude_plugins': 'Exclude plugin data from the import process (default = False)', 'exclude_plugins': 'Exclude plugin data from the import process (default = False)',
'skip_migrations': 'Skip the migration step after clearing data (default = False)', 'skip_migrations': 'Skip the migration step after clearing data (default = False)',
'verbose': 'Print verbose output from management commands',
}, },
pre=[wait], pre=[wait],
post=[rebuild_models, rebuild_thumbnails], post=[rebuild_models, rebuild_thumbnails],
@@ -1230,6 +1304,7 @@ def import_records(
exclude_plugins: bool = False, exclude_plugins: bool = False,
ignore_nonexistent: bool = False, ignore_nonexistent: bool = False,
skip_migrations: bool = False, skip_migrations: bool = False,
verbose: bool = False,
): ):
"""Import database records from a file.""" """Import database records from a file."""
# Get an absolute path to the supplied filename # Get an absolute path to the supplied filename
@@ -1243,10 +1318,10 @@ def import_records(
sys.exit(1) sys.exit(1)
if clear: if clear:
delete_data(c, force=True, migrate=True) delete_data(c, force=True, migrate=True, verbose=verbose)
if not skip_migrations: if not skip_migrations:
migrate(c) migrate(c, verbose=verbose)
info(f"Importing database records from '{target}'") info(f"Importing database records from '{target}'")
@@ -1274,6 +1349,7 @@ def import_records(
) -> tempfile.NamedTemporaryFile: ) -> tempfile.NamedTemporaryFile:
"""Helper function to save data to a temporary file, and then load into the database.""" """Helper function to save data to a temporary file, and then load into the database."""
nonlocal ignore_nonexistent nonlocal ignore_nonexistent
nonlocal verbose
nonlocal c nonlocal c
# Skip if there is no data to load # Skip if there is no data to load
@@ -1299,7 +1375,7 @@ def import_records(
if excludes: if excludes:
cmd += f' -i {excludes}' cmd += f' -i {excludes}'
manage(c, cmd, pty=True) manage(c, cmd, pty=True, verbose=verbose)
# Iterate through each entry in the provided data file, and separate out into different categories based on the model type # Iterate through each entry in the provided data file, and separate out into different categories based on the model type
for entry in data: for entry in data:
@@ -1363,22 +1439,22 @@ def import_records(
help={ help={
'force': 'Force deletion of all data without confirmation', 'force': 'Force deletion of all data without confirmation',
'migrate': 'Run migrations before deleting data (default = False)', 'migrate': 'Run migrations before deleting data (default = False)',
'verbose': 'Print verbose output from management commands',
} }
) )
def delete_data(c, force: bool = False, migrate: bool = False): def delete_data(c, force: bool = False, migrate: bool = False, verbose: bool = False):
"""Delete all database records! """Delete all database records!
Warning: This will REALLY delete all records in the database!! Warning: This will REALLY delete all records in the database!!
""" """
info('Deleting all data from InvenTree database...') info('Deleting existing data from InvenTree database...')
if migrate: if migrate:
manage(c, 'migrate --run-syncdb') manage(c, 'migrate --run-syncdb', verbose=verbose)
if force: manage(c, f'flush{" --noinput" if force else ""}', verbose=verbose)
manage(c, 'flush --noinput')
else: success('Existing data deleted')
manage(c, 'flush')
@task(post=[rebuild_models, rebuild_thumbnails]) @task(post=[rebuild_models, rebuild_thumbnails])
@@ -1640,6 +1716,7 @@ def test(
'validate_files': 'Validate media files are correctly copied', 'validate_files': 'Validate media files are correctly copied',
'use_ssh': 'Use SSH protocol for cloning the demo dataset (requires SSH key)', 'use_ssh': 'Use SSH protocol for cloning the demo dataset (requires SSH key)',
'branch': 'Specify branch of demo-dataset to clone (default = main)', 'branch': 'Specify branch of demo-dataset to clone (default = main)',
'verbose': 'Print verbose output from management commands',
} }
) )
def setup_test( def setup_test(
@@ -1648,6 +1725,7 @@ def setup_test(
dev=False, dev=False,
validate_files=False, validate_files=False,
use_ssh=False, use_ssh=False,
verbose=False,
path='inventree-demo-dataset', path='inventree-demo-dataset',
branch='main', branch='main',
): ):
@@ -1657,13 +1735,12 @@ def setup_test(
) )
if not ignore_update: if not ignore_update:
update(c) update(c, verbose=verbose)
template_dir = local_dir().joinpath(path) template_dir = local_dir().joinpath(path)
# Remove old data directory # Remove old data directory
if template_dir.exists(): if template_dir.exists():
info('Removing old data ...')
run(c, f'rm {template_dir} -r') run(c, f'rm {template_dir} -r')
URL = 'https://github.com/inventree/demo-dataset' URL = 'https://github.com/inventree/demo-dataset'
@@ -1678,11 +1755,16 @@ def setup_test(
# Make sure migrations are done - might have just deleted sqlite database # Make sure migrations are done - might have just deleted sqlite database
if not ignore_update: if not ignore_update:
migrate(c) migrate(c, verbose=verbose)
# Load data # Load data
info('Loading database records ...') info('Loading database records ...')
import_records(c, filename=template_dir.joinpath('inventree_data.json'), clear=True) import_records(
c,
filename=template_dir.joinpath('inventree_data.json'),
clear=True,
verbose=verbose,
)
# Copy media files # Copy media files
src = template_dir.joinpath('media') src = template_dir.joinpath('media')
@@ -1718,7 +1800,7 @@ def setup_test(
# Set up development setup if flag is set # Set up development setup if flag is set
if dev: if dev:
setup_dev(c) setup_dev(c, verbose=verbose)
@task( @task(
@@ -1929,7 +2011,7 @@ def frontend_build(c):
Args: Args:
c: Context variable c: Context variable
""" """
info('Building frontend') info('Building frontend...')
yarn(c, 'yarn run build') yarn(c, 'yarn run build')
def write_info(path: Path, content: str): def write_info(path: Path, content: str):
@@ -1957,6 +2039,8 @@ def frontend_build(c):
except Exception: except Exception:
warning('Failed to write frontend version marker') warning('Failed to write frontend version marker')
success('Frontend build complete')
@task @task
def frontend_server(c): def frontend_server(c):