Migrate from "poetry-python" to "managed-django-project"

* Remove poetry, pytest and devshell
* Use pip-tools, unittests and https://github.com/jedie/manage_django_project
pull/141/head
JensDiemer 2023-07-21 16:58:05 +02:00
rodzic 0b38878144
commit c6303556a9
27 zmienionych plików z 2058 dodań i 3666 usunięć

Wyświetl plik

@ -12,6 +12,7 @@ on:
jobs:
test:
name: 'Python ${{ matrix.python-version }} Django ${{ matrix.django-version }}'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@ -34,23 +35,29 @@ jobs:
with:
python-version: '${{ matrix.python-version }}'
cache: 'pip' # caching pip dependencies
cache-dependency-path: '**/poetry.lock'
cache-dependency-path: '**/requirements.*.txt'
- name: 'Bootstrap'
# The first manage.py call will create the .venv
run: |
python3 devshell.py quit
./manage.py version
- name: 'Install Browsers for Playwright tests'
- name: 'Install Playwright browsers'
run: |
python3 devshell.py playwright_install
.venv/bin/playwright install
- name: 'List installed packages'
- name: 'Display all Django commands'
run: |
python3 devshell.py list_venv_packages
./manage.py --help
- name: 'Run tests with Python v${{ matrix.python-version }}'
# FIXME:
#- name: 'Safety'
# run: |
# ./manage.py safety
- name: 'Python ${{ matrix.python-version }}'
run: |
python3 devshell.py pytest -vv
./manage.py tox -e $(echo py${{ matrix.python-version }} | tr -d .)
- name: 'Upload coverage report'
uses: codecov/codecov-action@v3

Wyświetl plik

@ -156,6 +156,8 @@ Files are separated into: "/src/" and "/development/"
Remove ["/development/"](https://github.com/jedie/PyInventory/tree/v0.18.1/deployment) (unmaintained "docker-compose" installation),
please use YunoHost ;)
Removed django-processinfo and django-axes in test project
## history
@ -164,7 +166,7 @@ please use YunoHost ;)
* tbc
* [v0.19.0 - 15.06.2023](https://github.com/jedie/PyInventory/compare/v0.18.1...v0.19.0)
* Update to Django 4.2
* remove django-processinfo and "/development/"
* remove django-processinfo, django-axes and "/development/"
* Bugfix ItemModelAdmin
* [v0.18.1 - 15.06.2023](https://github.com/jedie/PyInventory/compare/v0.18.0...v0.18.1)
* Update requirements

Wyświetl plik

@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""
developer shell
~~~~~~~~~~~~~~~
Just call this file, and the magic happens ;)
This file is from: https://pypi.org/project/dev-shell/
Source: https://github.com/jedie/dev-shell/blob/main/devshell.py
:copyleft: 2021-2022 by Jens Diemer
:license: GNU GPL v3 or above
"""
import argparse
import hashlib
import signal
import subprocess
import sys
import venv
from pathlib import Path
try:
import ensurepip # noqa
except ModuleNotFoundError as err:
print(err)
print('-' * 100)
print('Error: Pip not available!')
print('Hint: "apt-get install python3-venv"\n')
raise
assert sys.version_info >= (3, 7), 'Python version is too old!'
if sys.platform == 'win32': # wtf
# Files under Windows, e.g.: .../.venv/Scripts/python.exe
BIN_NAME = 'Scripts'
FILE_EXT = '.exe'
else:
# Files under Linux/Mac and all other than Windows, e.g.: .../.venv/bin/python
BIN_NAME = 'bin'
FILE_EXT = ''
BASE_PATH = Path(__file__).parent
VENV_PATH = BASE_PATH / '.venv'
BIN_PATH = VENV_PATH / BIN_NAME
PYTHON_PATH = BIN_PATH / f'python{FILE_EXT}'
PIP_PATH = BIN_PATH / f'pip{FILE_EXT}'
POETRY_PATH = BIN_PATH / f'poetry{FILE_EXT}'
DEP_LOCK_PATH = BASE_PATH / 'poetry.lock'
DEP_HASH_PATH = VENV_PATH / '.dep_hash'
# script file defined in pyproject.toml as [tool.poetry.scripts]
# (Under Windows: ".exe" not added!)
PROJECT_SHELL_SCRIPT = BIN_PATH / 'devshell'
def get_dep_hash():
""" Get SHA512 hash from poetry.lock content. """
return hashlib.sha512(DEP_LOCK_PATH.read_bytes()).hexdigest()
def store_dep_hash():
""" Generate /.venv/.dep_hash """
DEP_HASH_PATH.write_text(get_dep_hash())
def venv_up2date():
""" Is existing .venv is up-to-date? """
if DEP_HASH_PATH.is_file():
return DEP_HASH_PATH.read_text() == get_dep_hash()
return False
def verbose_check_call(*popen_args):
popen_args = [str(arg) for arg in popen_args] # e.g.: Path() -> str for python 3.7
print(f'\n+ {" ".join(popen_args)}\n')
return subprocess.check_call(popen_args)
def noop_signal_handler(signal_num, frame):
"""
Signal handler that does nothing: Used to ignore "Ctrl-C" signals
"""
pass
def main(argv):
assert DEP_LOCK_PATH.is_file(), f'File not found: "{DEP_LOCK_PATH}" !'
if len(argv) == 2 and argv[1] in ('--update', '--help'):
parser = argparse.ArgumentParser(
prog=Path(__file__).name,
description='Developer shell',
epilog='...live long and prosper...'
)
parser.add_argument(
'--update', default=False, action='store_true',
help='Force create/upgrade virtual environment'
)
parser.add_argument(
'command_args',
nargs=argparse.ZERO_OR_MORE,
help='arguments to pass to dev-setup shell/cli',
)
options = parser.parse_args(argv)
force_update = options.update
extra_args = argv[2:]
else:
force_update = False
extra_args = argv[1:]
# Create virtual env in ".../.venv/":
if not PYTHON_PATH.is_file() or force_update:
print('Create virtual env here:', VENV_PATH.absolute())
builder = venv.EnvBuilder(symlinks=True, upgrade=True, with_pip=True)
builder.create(env_dir=VENV_PATH)
# install/update "pip" and "poetry":
if not POETRY_PATH.is_file() or force_update:
# Note: Under Windows pip.exe can't replace this own .exe file, so use the module way:
verbose_check_call(PYTHON_PATH, '-m', 'pip', 'install', '-U', 'pip', 'setuptools')
verbose_check_call(PIP_PATH, 'install', 'poetry!=1.2.0')
# install via poetry, if:
# 1. .venv not exists
# 2. "--update" used
# 3. poetry.lock file was changed
if not PROJECT_SHELL_SCRIPT.is_file() or force_update or not venv_up2date():
verbose_check_call(POETRY_PATH, 'install')
store_dep_hash()
# The cmd2 shell should not abort on Ctrl-C => ignore "Interrupt from keyboard" signal:
signal.signal(signal.SIGINT, noop_signal_handler)
# Run project cmd shell via "setup.py" entrypoint:
# (Call it via python, because Windows sucks calling the file direct)
try:
verbose_check_call(PYTHON_PATH, PROJECT_SHELL_SCRIPT, *extra_args)
except subprocess.CalledProcessError as err:
sys.exit(err.returncode)
if __name__ == '__main__':
main(sys.argv)

Wyświetl plik

@ -4,7 +4,4 @@
:license: GNU GPL v3 or above, see LICENSE for more details.
"""
from importlib.metadata import version
__version__ = version('PyInventory')
__version__ = '0.19.0'

Wyświetl plik

@ -1,5 +0,0 @@
from django_tools.management.commands.run_testserver import Command as RunServerCommand
class Command(RunServerCommand):
pass

Wyświetl plik

@ -7,3 +7,6 @@ import inventory
PACKAGE_ROOT = Path(inventory.__file__).parent.parent
assert_is_dir(PACKAGE_ROOT / 'inventory')
__version__ = inventory.__version__

Wyświetl plik

@ -0,0 +1,17 @@
"""
Allow your-cool-package to be executable
through `python -m inventory`.
"""
from manage_django_project.manage import execute_django_from_command_line
def main():
"""
entrypoint installed via pyproject.toml and [project.scripts] section.
Must be set in ./manage.py and PROJECT_SHELL_SCRIPT
"""
execute_django_from_command_line()
if __name__ == '__main__':
main()

Wyświetl plik

@ -1,265 +0,0 @@
import os
from pathlib import Path
import cmd2
from creole.setup_utils import update_rst_readme
from dev_shell.base_cmd2_app import DevShellBaseApp, run_cmd2_app
from dev_shell.command_sets import DevShellBaseCommandSet
from dev_shell.command_sets.dev_shell_commands import DevShellCommandSet as OriginDevShellCommandSet
from dev_shell.config import DevShellConfig
from dev_shell.utils.assertion import assert_is_dir
from dev_shell.utils.colorful import blue, bright_yellow, print_error
from dev_shell.utils.subprocess_utils import argv2str, make_relative_path, verbose_check_call
import inventory
from inventory_project import PACKAGE_ROOT
from inventory_project.manage import main
class TempCwd:
def __init__(self, cwd: Path):
assert_is_dir(cwd)
self.cwd = cwd
def __enter__(self):
self.old_cwd = Path().cwd()
os.chdir(self.cwd)
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.old_cwd)
def call_manage_py(*args, cwd):
print()
print('_' * 100)
cwd_rel = make_relative_path(cwd, relative_to=Path.cwd())
print(f'+ {cwd_rel}$ {bright_yellow("manage.py")} {blue(argv2str(args))}\n')
args = list(args)
args.insert(0, 'manage.py') # Needed for argparse!
with TempCwd(cwd):
try:
main(argv=args)
except SystemExit as err:
print_error(f'finished with exit code {err}')
except BaseException as err:
print_error(err)
@cmd2.with_default_category('PyInventory commands')
class PyInventoryCommandSet(DevShellBaseCommandSet):
def do_manage(self, statement: cmd2.Statement):
"""
Call PyInventory test "manage.py"
"""
call_manage_py(*statement.arg_list, cwd=PACKAGE_ROOT)
def do_run_testserver(self, statement: cmd2.Statement):
"""
Start Django dev server with the test project
"""
# Start the "[tool.poetry.scripts]" script via subprocess
# This works good with django dev server reloads
verbose_check_call('run_testserver', *statement.arg_list, cwd=PACKAGE_ROOT, timeout=None)
def do_makemessages(self, statement: cmd2.Statement):
"""
Make and compile locales message files
"""
call_manage_py(
'makemessages',
'--all',
'--no-location',
'--no-obsolete',
'--ignore=.*',
'--ignore=htmlcov',
'--ignore=volumes',
cwd=PACKAGE_ROOT / 'inventory',
)
call_manage_py('compilemessages', cwd=PACKAGE_ROOT / 'inventory')
def do_update_rst_readme(self, statement: cmd2.Statement):
"""
update README.rst from README.creole
"""
update_rst_readme(package_root=PACKAGE_ROOT, filename='README.creole')
def do_ckeditor_info(self, statement: cmd2.Statement):
"""
Print some information about CKEditor
"""
import ckeditor
ckeditor_path = Path(ckeditor.__file__).parent
print('Django-CKEditor path:', ckeditor_path)
build_config_path = Path(ckeditor_path, 'static/ckeditor/ckeditor/build-config.js')
print('Build config:', build_config_path)
plugins_path = Path(ckeditor_path, 'static/ckeditor/ckeditor/plugins')
print('Plugin path:', plugins_path)
assert plugins_path.is_dir()
plugins = {item.name for item in plugins_path.iterdir() if item.is_dir()}
in_plugins = False
with build_config_path.open('r') as f:
for line in f:
line = line.strip()
if not line:
continue
if line == 'plugins : {':
in_plugins = True
continue
if in_plugins:
if line == '},':
break
plugin_name = line.split(':', 1)[0].strip(" '")
plugins.add(plugin_name)
print("'removePlugins': (")
for plugin_name in sorted(plugins):
print(f" '{plugin_name}',")
print(')')
def do_fill_verbose_name_translations(self, statement: cmd2.Statement):
"""
Auto fill "verbose_name" translations:
Just copy the model field name as translation.
"""
MESSAGE_MAP = {'id': 'ID'}
for lang_code in ('de', 'en'):
print('_' * 100)
print(lang_code)
po_file_path = PACKAGE_ROOT / f'inventory/locale/{lang_code}/LC_MESSAGES/django.po'
old_content = []
new_content = []
with po_file_path.open('r') as f:
for line in f:
old_content.append(line)
if line.startswith('msgid "'):
msgstr = ''
msgid = line[7:-2]
try:
model, attribute, kind = msgid.strip().split('.')
except ValueError:
pass
else:
if kind == 'verbose_name':
if attribute in MESSAGE_MAP:
msgstr = MESSAGE_MAP[attribute]
else:
words = attribute.replace('_', ' ').split(' ')
msgstr = ' '.join(i.capitalize() for i in words)
elif kind == 'help_text':
msgstr = ' ' # "hide" empty "help_text"
elif (line == 'msgstr ""\n' or line == 'msgstr " "\n') and msgstr:
line = f'msgstr "{msgstr}"\n'
line = line.replace('Content Tonie', 'Content-Tonie')
new_content.append(line)
if new_content == old_content:
print('Nothing to do, ok.')
return
with po_file_path.open('w') as f:
f.write(''.join(new_content))
print(f'updated: {po_file_path}')
def do_update_test_snapshots(self, statement: cmd2.Statement):
"""
Update all test snapshot files by run tests with RAISE_SNAPSHOT_ERRORS=0
"""
verbose_check_call(
'pytest',
*statement.arg_list,
cwd=self.config.base_path,
exit_on_error=True,
extra_env={
# https://github.com/boxine/bx_py_utils#notes-about-snapshot
'RAISE_SNAPSHOT_ERRORS': '0'
},
)
def do_playwright(self, statement: cmd2.Statement):
"""
Interact with "playwright"
"""
verbose_check_call('playwright', *statement.arg_list, cwd=self.config.base_path)
def do_playwright_install(self, statement: cmd2.Statement):
"""
Update all test snapshot files by run tests with RAISE_SNAPSHOT_ERRORS=0
"""
args = statement.arg_list
if not args:
args = ['chromium', 'firefox']
verbose_check_call('playwright', 'install', *args, cwd=self.config.base_path)
def do_playwright_inspector(self, statement: cmd2.Statement):
"""
Run Playwright test with the Inspector (Excludes all non Playwright tests)
"""
verbose_check_call(
'pytest',
'-s',
'-m',
'playwright',
*statement.arg_list,
cwd=self.config.base_path,
exit_on_error=True,
extra_env={
'PWDEBUG': '1',
},
timeout=None,
)
def do_seed_data(self, statement: cmd2.Statement):
"""
Fill database with example data
"""
args = ['seed_data', *statement.arg_list]
call_manage_py(*args, cwd=PACKAGE_ROOT)
class DevShellCommandSet(OriginDevShellCommandSet):
pass
class DevShellApp(DevShellBaseApp):
pass
def get_devshell_app_kwargs():
"""
Generate the kwargs for the cmd2 App.
(Separated because we needs the same kwargs in tests)
"""
config = DevShellConfig(package_module=inventory)
# initialize all CommandSet() with context:
kwargs = dict(config=config)
app_kwargs = dict(
config=config,
command_sets=[
PyInventoryCommandSet(**kwargs),
DevShellCommandSet(**kwargs),
],
)
return app_kwargs
def devshell_cmdloop():
"""
Entry point to start the "dev-shell" cmd2 app.
Used in: [tool.poetry.scripts]
"""
app = DevShellApp(**get_devshell_app_kwargs())
run_cmd2_app(app) # Run a cmd2 App as CLI or shell

Wyświetl plik

@ -4,14 +4,18 @@
Django settings for local development
"""
import os as __os
import sys as __sys
from inventory_project.settings.base import * # noqa
from inventory_project.settings.prod import * # noqa
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
# Serve static/media files for local development:
SERVE_FILES = True
# Disable caches:
@ -25,37 +29,33 @@ ALLOWED_HOSTS = INTERNAL_IPS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_PATH / 'PyInventory-database.sqlite3'),
'NAME': str(BASE_PATH / 'inventory-database.sqlite3'),
# https://docs.djangoproject.com/en/dev/ref/databases/#database-is-locked-errors
'timeout': 30,
}
}
print(f'Use Database: {DATABASES["default"]["NAME"]!r}', file=__sys.stderr)
# _____________________________________________________________________________
# Disable security features, because development server doesn't support HTTPS
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
SECURE_PROXY_SSL_HEADER = None
SECURE_SSL_REDIRECT = False
SECURE_HSTS_PRELOAD = False
SECURE_HSTS_SECONDS = 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
# _____________________________________________________________________________
# AlwaysLoggedInAsSuperUser
DEFAULT_USERNAME = 'local-test-superuser'
DEFAULT_USERPASS = 'test'
DEFAULT_USEREMAIL = 'nobody@local.intranet'
# Download map via geotiler in inventory.gpx_tools.gpxpy2map.generate_map
MAP_DOWNLOAD = True
if __os.environ.get('AUTOLOGIN') == '1':
# Auto login for dev. server:
MIDDLEWARE = MIDDLEWARE.copy()
MIDDLEWARE += ['django_tools.middlewares.local_auto_login.AlwaysLoggedInAsSuperUserMiddleware']
# _____________________________________________________________________________
# Manage Django Project
INSTALLED_APPS.append('manage_django_project')
MIDDLEWARE = MIDDLEWARE.copy()
MIDDLEWARE.append('django_tools.middlewares.local_auto_login.AlwaysLoggedInAsSuperUserMiddleware')
# _____________________________________________________________________________
# Django-Debug-Toolbar
INSTALLED_APPS.copy()
INSTALLED_APPS += ['debug_toolbar']
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
DEBUG_TOOLBAR_PATCH_SETTINGS = True

Wyświetl plik

@ -1,6 +1,6 @@
'''
"""
Base Django settings
'''
"""
import logging
from pathlib import Path as __Path
@ -12,28 +12,12 @@ from django.utils.translation import gettext_lazy as _
###############################################################################
# Build paths relative to the project root:
PROJECT_PATH = __Path(__file__).parent.parent.parent
print(f'PROJECT_PATH:{PROJECT_PATH}')
if __Path('/.dockerenv').is_file():
# We are inside a docker container
BASE_PATH = __Path('/django_volumes')
assert BASE_PATH.is_dir()
else:
# Build paths relative to the current working directory:
BASE_PATH = __Path().cwd().resolve()
BASE_PATH = __Path(__file__).parent.parent.parent
print(f'BASE_PATH:{BASE_PATH}')
# Paths with Django dev. server:
# BASE_PATH...: .../django-for-runners
# PROJECT_PATH: .../django-for-runners
#
# Paths in Docker container:
# BASE_PATH...: /for_runners_volumes
# PROJECT_PATH: /usr/local/lib/python3.9/site-packages
assert __Path(BASE_PATH, 'inventory_project').is_dir()
###############################################################################
# PyInventory:
# Max length of Item/Location "path name" in change list:
TREE_PATH_STR_MAX_LENGTH = 70
@ -43,10 +27,9 @@ TREE_PATH_STR_MAX_LENGTH = 70
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
TEMPLATE_DEBUG = False
# Serve static/media files by Django?
# In production Caddy should serve this!
# In production the Webserver should serve this!
SERVE_FILES = False
@ -70,7 +53,6 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'bx_django_utils', # https://github.com/boxine/bx_django_utils
'import_export', # https://github.com/django-import-export/django-import-export
'dbbackup', # https://github.com/django-dbbackup/django-dbbackup
@ -80,7 +62,6 @@ INSTALLED_APPS = [
'reversion_compare', # https://github.com/jedie/django-reversion-compare
'tagulous', # https://github.com/radiac/django-tagulous
'adminsortable2', # https://github.com/jrief/django-admin-sortable2
'axes', # https://github.com/jazzband/django-axes
# https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_app
'django_tools.serve_media_app.apps.UserMediaFilesConfig',
# https://github.com/jedie/django-tools/tree/master/django_tools/model_version_protect
@ -90,10 +71,8 @@ INSTALLED_APPS = [
ROOT_URLCONF = 'inventory_project.urls'
WSGI_APPLICATION = 'inventory_project.wsgi.application'
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
'axes.backends.AxesBackend',
'django.contrib.auth.backends.ModelBackend',
]
@ -107,20 +86,26 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'axes.middleware.AxesMiddleware', # AxesMiddleware should be the last middleware
]
__TEMPLATE_DIR = __Path(BASE_PATH, 'inventory_project', 'templates')
assert __TEMPLATE_DIR.is_dir(), f'Directory not exists: {__TEMPLATE_DIR}'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [str(__Path(PROJECT_PATH, 'inventory_project', 'templates'))],
"DIRS": [str(__TEMPLATE_DIR)],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.template.context_processors.media',
'django.template.context_processors.csrf',
'django.template.context_processors.tz',
'django.template.context_processors.static',
'inventory.context_processors.inventory_version_string',
],
},
@ -176,6 +161,16 @@ STATIC_ROOT = str(__Path(BASE_PATH, 'static'))
MEDIA_URL = '/media/'
MEDIA_ROOT = str(__Path(BASE_PATH, 'media'))
# _____________________________________________________________________________
# Cache Backend
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
# _____________________________________________________________________________
# Django-dbbackup
@ -392,7 +387,6 @@ LOGGING = {
'loggers': {
'': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
'django': {'handlers': ['console'], 'level': 'INFO', 'propagate': False},
'axes': {'handlers': ['console'], 'level': 'WARNING', 'propagate': False},
'django_tools': {'handlers': ['console'], 'level': 'INFO', 'propagate': False},
'inventory': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
},

Wyświetl plik

@ -1,6 +1,18 @@
# flake8: noqa: E405, F403
# flake8: noqa: E405
"""
Settings used to run tests
"""
import os
from inventory_project.settings.base import *
from inventory_project.settings.prod import * # noqa
# _____________________________________________________________________________
# Manage Django Project
INSTALLED_APPS.append('manage_django_project')
# _____________________________________________________________________________
DATABASES = {
@ -10,18 +22,24 @@ DATABASES = {
}
}
SECRET_KEY = 'No individual secret... But this settings should only be used in tests ;)'
DEBUG = True
# Run the tests as on production: Without DBEUG:
DEBUG = False
TEMPLATE_DEBUG = False
# Speedup tests by change the Password hasher:
PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher',)
ALLOWED_HOSTS = ('127.0.0.1', '0.0.0.0', 'localhost')
LOGGING['formatters']['colored'][
'format'
] = '%(log_color)s%(name)s %(levelname)8s %(cut_path)s:%(lineno)-3s %(message)s'
# _____________________________________________________________________________
# https://github.com/microsoft/playwright-pytest/issues/115
SECURE_SSL_REDIRECT = False
# All tests should use django-override-storage!
# Set root to not existing path, so that wrong tests will fail:
STATIC_ROOT = '/not/exists/static/'
MEDIA_ROOT = '/not/exists/media/'
# _____________________________________________________________________________
# Playwright
# Avoid django.core.exceptions.SynchronousOnlyOperation. Playwright uses an event loop,
# even when using he sync API. Django only checks whether _any_ event loop is running,
# but not if _itself_ is running in an even loop.
# see https://github.com/microsoft/playwright-python/issues/439#issuecomment-763339612.
os.environ.setdefault('DJANGO_ALLOW_ASYNC_UNSAFE', '1')

Wyświetl plik

@ -1,32 +0,0 @@
import os
import tempfile
import pytest
from PIL import Image
# Avoid django.core.exceptions.SynchronousOnlyOperation:
os.environ.setdefault('DJANGO_ALLOW_ASYNC_UNSAFE', '1')
@pytest.fixture(scope='session')
def browser_context_args(browser_context_args):
browser_context_args.update(
dict(
ignore_https_errors=True,
locale='en_US',
timezone_id='Europe/Berlin',
)
)
return browser_context_args
@pytest.fixture(scope='function')
def png_image():
format = 'png'
with tempfile.NamedTemporaryFile(prefix='test_image', suffix=f'.{format}') as tmp:
image_size = (1, 1)
pil_image = Image.new('RGB', image_size)
pil_image.save(tmp, format=format)
tmp.seek(0)
yield tmp

Wyświetl plik

@ -1,5 +1,8 @@
import tempfile
from django.contrib.auth.models import User
from model_bakery import baker
from PIL import Image
from inventory.permissions import get_or_create_normal_user_group
@ -18,3 +21,20 @@ def get_normal_user():
user.groups.set([group])
user.full_clean()
return user
class TempImageFile:
def __init__(self, prefix='test_image', format='png', size=(1, 1)):
self.format = format
self.image_size = size
self.temp = tempfile.NamedTemporaryFile(prefix=prefix, suffix=f'.{format}')
def __enter__(self):
self.temp_file = self.temp.__enter__()
pil_image = Image.new('RGB', self.image_size)
pil_image.save(self.temp_file, format=self.format)
self.temp_file.seek(0)
return self.temp_file
def __exit__(self, exc_type, exc_val, exc_tb):
self.temp_file.__exit__(exc_type, exc_val, exc_tb)

Wyświetl plik

@ -8,6 +8,7 @@ from django.template.defaulttags import CsrfTokenNode, NowNode
from django.test import TestCase, override_settings
from django.utils import timezone
from django_tools.unittest_utils.mockup import ImageDummy
from override_storage import locmem_stats_override_storage
from reversion.models import Revision
from inventory.models import ItemImageModel, ItemModel
@ -157,32 +158,36 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
}
)
response = self.client.post(
path='/admin/inventory/itemmodel/add/',
data=post_data,
)
self.assertRedirects(response, expected_url='/admin/inventory/itemmodel/')
with locmem_stats_override_storage() as storage_stats:
response = self.client.post(
path='/admin/inventory/itemmodel/add/',
data=post_data,
)
self.assertRedirects(response, expected_url='/admin/inventory/itemmodel/')
data = list(ItemModel.objects.values_list('kind__name', 'name'))
assert data == [('kind', 'name')]
data = list(ItemModel.objects.values_list('kind__name', 'name'))
assert data == [('kind', 'name')]
item = ItemModel.objects.first()
item = ItemModel.objects.first()
self.assert_messages(
response,
expected_messages=[
f'The Item “<a href="/admin/inventory/itemmodel/{item.pk}/change/">name</a>”'
' was added successfully.'
],
)
self.assert_messages(
response,
expected_messages=[
f'The Item “<a href="/admin/inventory/itemmodel/{item.pk}/change/">name</a>”'
' was added successfully.'
],
)
assert item.user_id == self.normaluser.pk
assert item.user_id == self.normaluser.pk
assert ItemImageModel.objects.count() == 1
image = ItemImageModel.objects.first()
assert image.name == 'test.png'
assert image.item == item
assert image.user_id == self.normaluser.pk
assert ItemImageModel.objects.count() == 1
image = ItemImageModel.objects.first()
assert image.name == 'test.png'
assert image.item == item
assert image.user_id == self.normaluser.pk
# Test image file should be stored:
self.assertEqual(storage_stats.save_cnt, 1)
def test_auto_group_items(self):
self.client.force_login(self.normaluser)

Wyświetl plik

@ -298,7 +298,7 @@
Create date:
</label>
<div class="readonly">
Jan. 1, 2000, 1:11 a.m.
Jan. 1, 2000, 1:10 a.m.
</div>
</div>
<div class="help">
@ -315,7 +315,7 @@
Last update:
</label>
<div class="readonly">
Jan. 1, 2000, 1:12 a.m.
Jan. 1, 2000, 1:11 a.m.
</div>
</div>
<div class="help">

Wyświetl plik

@ -6,6 +6,7 @@ from django.template.defaulttags import CsrfTokenNode, NowNode
from django.test import TestCase, override_settings
from django_tools.unittest_utils.mockup import ImageDummy
from model_bakery import baker
from override_storage import locmem_stats_override_storage
from reversion.models import Revision
from inventory.models import MemoImageModel, MemoModel
@ -99,50 +100,54 @@ class AdminTestCase(HtmlAssertionMixin, TestCase):
img = ImageDummy(width=1, height=1, format='png').in_memory_image_file(filename='test.png')
response = self.client.post(
path='/admin/inventory/memomodel/add/',
data={
'version': 0, # VersionProtectBaseModel field
'name': 'The Memo Name',
'memo': 'This is a test Memo',
'memoimagemodel_set-TOTAL_FORMS': '1',
'memoimagemodel_set-INITIAL_FORMS': '0',
'memoimagemodel_set-MIN_NUM_FORMS': '0',
'memoimagemodel_set-MAX_NUM_FORMS': '1000',
'memoimagemodel_set-0-position': '0',
'memoimagemodel_set-__prefix__-position': '0',
'memoimagemodel_set-0-image': img,
'memofilemodel_set-TOTAL_FORMS': '0',
'memofilemodel_set-INITIAL_FORMS': '0',
'memofilemodel_set-MIN_NUM_FORMS': '0',
'memofilemodel_set-MAX_NUM_FORMS': '1000',
'memofilemodel_set-__prefix__-position': '0',
'memolinkmodel_set-TOTAL_FORMS': '0',
'memolinkmodel_set-INITIAL_FORMS': '0',
'memolinkmodel_set-MIN_NUM_FORMS': '0',
'memolinkmodel_set-MAX_NUM_FORMS': '1000',
'memolinkmodel_set-__prefix__-position': '0',
'_save': 'Save',
},
)
assert response.status_code == 302, response.content.decode('utf-8') # Form error?
memo = MemoModel.objects.first() or MemoModel()
self.assert_messages(
response,
expected_messages=[
f'The Memo “<a href="/admin/inventory/memomodel/{memo.pk}/change/">The Memo Name</a>”'
' was added successfully.'
],
)
self.assertRedirects(response, expected_url='/admin/inventory/memomodel/')
with locmem_stats_override_storage() as storage_stats:
response = self.client.post(
path='/admin/inventory/memomodel/add/',
data={
'version': 0, # VersionProtectBaseModel field
'name': 'The Memo Name',
'memo': 'This is a test Memo',
'memoimagemodel_set-TOTAL_FORMS': '1',
'memoimagemodel_set-INITIAL_FORMS': '0',
'memoimagemodel_set-MIN_NUM_FORMS': '0',
'memoimagemodel_set-MAX_NUM_FORMS': '1000',
'memoimagemodel_set-0-position': '0',
'memoimagemodel_set-__prefix__-position': '0',
'memoimagemodel_set-0-image': img,
'memofilemodel_set-TOTAL_FORMS': '0',
'memofilemodel_set-INITIAL_FORMS': '0',
'memofilemodel_set-MIN_NUM_FORMS': '0',
'memofilemodel_set-MAX_NUM_FORMS': '1000',
'memofilemodel_set-__prefix__-position': '0',
'memolinkmodel_set-TOTAL_FORMS': '0',
'memolinkmodel_set-INITIAL_FORMS': '0',
'memolinkmodel_set-MIN_NUM_FORMS': '0',
'memolinkmodel_set-MAX_NUM_FORMS': '1000',
'memolinkmodel_set-__prefix__-position': '0',
'_save': 'Save',
},
)
assert response.status_code == 302, response.content.decode('utf-8') # Form error?
memo = MemoModel.objects.first() or MemoModel()
self.assert_messages(
response,
expected_messages=[
f'The Memo “<a href="/admin/inventory/memomodel/{memo.pk}/change/">The Memo Name</a>”'
' was added successfully.'
],
)
self.assertRedirects(response, expected_url='/admin/inventory/memomodel/')
data = list(MemoModel.objects.values_list('name', 'memo'))
assert data == [('The Memo Name', 'This is a test Memo')]
data = list(MemoModel.objects.values_list('name', 'memo'))
assert data == [('The Memo Name', 'This is a test Memo')]
assert memo.user_id == self.normaluser.pk
assert memo.user_id == self.normaluser.pk
assert MemoImageModel.objects.count() == 1
image = MemoImageModel.objects.first()
assert image.name == 'test.png'
assert image.memo == memo
assert image.user_id == self.normaluser.pk
assert MemoImageModel.objects.count() == 1
image = MemoImageModel.objects.first()
assert image.name == 'test.png'
assert image.memo == memo
assert image.user_id == self.normaluser.pk
# Test image file should be stored:
self.assertEqual(storage_stats.save_cnt, 1)

Wyświetl plik

@ -1,23 +0,0 @@
import filecmp
import shutil
from pathlib import Path
from unittest import TestCase
import dev_shell
from dev_shell.utils.assertion import assert_is_file
from inventory_project.dev_shell import PACKAGE_ROOT
class BootstrapTestCase(TestCase):
def test_our_bootstrap_is_up2date(self):
source_file_path = Path(dev_shell.__file__).parent / 'bootstrap-source.py'
assert_is_file(source_file_path)
own_bootstrap_file = PACKAGE_ROOT / 'devshell.py'
assert_is_file(own_bootstrap_file)
are_the_same = filecmp.cmp(source_file_path, own_bootstrap_file, shallow=False)
if not are_the_same:
shutil.copyfile(src=source_file_path, dst=own_bootstrap_file, follow_symlinks=False)
raise AssertionError(f'Bootstrap "{own_bootstrap_file}" updated!')

Wyświetl plik

@ -0,0 +1,60 @@
Documented commands (use 'help -v' for verbose/'help <topic>' for details):
adminsortable2
==============
reorder
ckeditor_uploader
=================
generateckeditorthumbnails
dbbackup
========
dbbackup dbrestore listbackups mediabackup mediarestore
django.contrib.auth
===================
changepassword createsuperuser
django.contrib.contenttypes
===========================
remove_stale_contenttypes
django.contrib.sessions
=======================
clearsessions
django.contrib.staticfiles
==========================
collectstatic findstatic runserver
django.core
===========
check flush optimizemigration squashmigrations
compilemessages inspectdb sendtestemail startapp
createcachetable loaddata showmigrations startproject
dbshell makemessages sqlflush test
diffsettings makemigrations sqlmigrate testserver
dumpdata migrate sqlsequencereset
inventory
=========
seed_data tree
manage_django_project
=====================
code_style install publish safety update_req
coverage project_info run_dev_server tox
reversion
=========
createinitialrevisions deleterevisions
tagulous
========
initial_tags
Uncategorized
=============
alias help history macro quit set shortcuts

Wyświetl plik

@ -1,47 +0,0 @@
import subprocess
import sys
from unittest import TestCase
from dev_shell.utils.assertion import assert_is_file
from inventory_project import PACKAGE_ROOT
def call_devshell_commands(*args):
dev_shell_py = PACKAGE_ROOT / 'devshell.py'
assert_is_file(dev_shell_py)
output = subprocess.check_output(
[sys.executable, str(dev_shell_py)] + list(args), stderr=subprocess.STDOUT, text=True
)
return output
class DevShellTestCase(TestCase):
def test_run_testserver(self):
output = call_devshell_commands('run_testserver', '--help')
assert 'Setup test project and run django developer server' in output
# From own run_testserver command:
assert '--nomakemigrations' in output
assert '--nomigrate' in output
# From django.core.management.commands.runserver command:
assert '[addrport]' in output
def test_run_testserver_invalid_addr(self):
output = call_devshell_commands(
'run_testserver',
'--nomigrate',
'--nomakemigrations',
'invalid:addr',
)
assert 'Call "runserver"' in output
assert 'is not a valid port number or address' in output
def test_manage_command(self):
output = call_devshell_commands('manage', 'diffsettings')
assert "DJANGO_SETTINGS_MODULE='inventory_project.settings.tests'" in output
assert f"PROJECT_PATH:{PACKAGE_ROOT}" in output
assert f"BASE_PATH:{PACKAGE_ROOT}" in output
assert f"PROJECT_PATH = PosixPath('{PACKAGE_ROOT}')" in output

Wyświetl plik

@ -1,43 +1,26 @@
from cmd2 import CommandResult
from cmd2_ext_test import ExternalTestMixin
from dev_shell.tests.fixtures import CmdAppBaseTestCase
from django import __version__
from inventory_project.dev_shell import DevShellApp, get_devshell_app_kwargs
import django
from bx_py_utils.test_utils.snapshot import assert_text_snapshot
from manage_django_project.tests.cmd2_test_utils import BaseShellTestCase
class DevShellAppTester(ExternalTestMixin, DevShellApp):
pass
class PyInventoryDevShellTestCase(BaseShellTestCase):
def test_help(self):
stdout, stderr = self.execute(command='help')
self.assertEqual(stderr, '')
self.assertIn('Documented commands', stdout)
# Django commands:
self.assertIn('django.core', stdout)
self.assertIn('makemessages', stdout)
self.assertIn('makemigrations', stdout)
class DevShellAppBaseTestCase(CmdAppBaseTestCase):
"""
Base class for dev-shell tests
"""
# manage_django_project:
self.assertIn('manage_django_project', stdout)
self.assertIn('run_dev_server', stdout)
def get_app_instance(self):
# Init the test app with the same kwargs as the real app
# see: dev_shell.cmd2app.devshell_cmdloop()
app = DevShellAppTester(**get_devshell_app_kwargs())
return app
# Own commands:
self.assertIn('inventory', stdout)
self.assertIn('seed_data', stdout)
self.assertIn('tree', stdout)
class PyInventoryDevShellTestCase(DevShellAppBaseTestCase):
def test_help_raw(self):
out = self.app.app_cmd('help')
assert isinstance(out, CommandResult)
assert 'Documented commands' in out.stdout
assert 'Documented commands' in out.stdout
def test_help_via_execute(self):
stdout, stderr = self.execute('help')
assert stderr == ''
assert 'Documented commands' in stdout
def test_manage(self):
stdout, stderr = self.execute('manage --version')
assert stderr == ''
assert 'manage.py --version' in stdout
assert __version__ in stdout
assert_text_snapshot(got=stdout, snapshot_name=f'test_command_shell_help_django{django.__version__}')

Wyświetl plik

@ -1,133 +1,157 @@
import pytest
from django.conf import settings
from bx_django_utils.test_utils.playwright import PlaywrightTestCase
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.http import HttpRequest
from playwright.sync_api import Page, expect
from django.test import override_settings
from override_storage import locmem_stats_override_storage
from playwright.sync_api import BrowserContext, expect
from inventory import __version__
from inventory.models import ItemImageModel, ItemLinkModel, ItemModel
from inventory_project.tests.fixtures import get_normal_user
from inventory_project.tests.fixtures import TempImageFile, get_normal_user
from inventory_project.tests.playwright_utils import login
@pytest.mark.playwright
def test_root_page(live_server, page: Page):
# https://github.com/microsoft/playwright-pytest/issues/115
assert settings.SECURE_SSL_REDIRECT is False
@override_settings(SECURE_SSL_REDIRECT=False)
class PlaywrightInventoryTestCase(PlaywrightTestCase):
def test_root_page(self):
context: BrowserContext = self.browser.new_context(
ignore_https_errors=True,
locale='en_US',
timezone_id='Europe/Berlin',
)
with context.new_page() as page:
page.goto(self.live_server_url)
expect(page).to_have_url(f'{self.live_server_url}/admin/login/?next=/admin/')
expect(page).to_have_title(f'Log in | PyInventory v{__version__}')
page.goto(live_server.url)
expect(page).to_have_url(f'{live_server}/admin/login/?next=/admin/')
expect(page).to_have_title(f'Log in | PyInventory v{__version__}')
def test_login(self):
username = 'a-user'
password = 'ThisIsNotAPassword!'
superuser = User.objects.create_superuser(username=username, password=password)
superuser.full_clean()
user = authenticate(request=HttpRequest(), username=username, password=password)
assert isinstance(user, User)
@pytest.mark.playwright
def test_login(live_server, page: Page):
username = 'a-user'
password = 'ThisIsNotAPassword!'
superuser = User.objects.create_superuser(username=username, password=password)
superuser.full_clean()
context: BrowserContext = self.browser.new_context(
ignore_https_errors=True,
locale='en_US',
timezone_id='Europe/Berlin',
)
with context.new_page() as page:
page.goto(self.live_server_url)
expect(page).to_have_url(f'{self.live_server_url}/admin/login/?next=/admin/')
expect(page).to_have_title(f'Log in | PyInventory v{__version__}')
user = authenticate(request=HttpRequest(), username=username, password=password)
assert isinstance(user, User)
page.type('#id_username', username)
page.type('#id_password', password)
page.locator('text=Log in').click()
page.goto(live_server.url)
expect(page).to_have_url(f'{live_server}/admin/login/?next=/admin/')
expect(page).to_have_title(f'Log in | PyInventory v{__version__}')
expect(page).to_have_url(f'{self.live_server_url}/admin/')
expect(page).to_have_title(f'Site administration | PyInventory v{__version__}')
page.type('#id_username', username)
page.type('#id_password', password)
page.locator('text=Log in').click()
def test_admin(self):
superuser = User.objects.create_superuser(username='foo', password='ThisIsNotAPassword!')
superuser.full_clean()
expect(page).to_have_url(f'{live_server}/admin/')
expect(page).to_have_title(f'Site administration | PyInventory v{__version__}')
context: BrowserContext = self.browser.new_context(
ignore_https_errors=True,
locale='en_US',
timezone_id='Europe/Berlin',
)
with context.new_page() as page:
login(page, self.client, url=self.live_server_url, user=superuser)
page.goto(f'{self.live_server_url}/admin/')
expect(page).to_have_url(f'{self.live_server_url}/admin/')
expect(page).to_have_title(f'Site administration | PyInventory v{__version__}')
@pytest.mark.playwright
def test_admin(live_server, client, page: Page):
superuser = User.objects.create_superuser(username='foo', password='ThisIsNotAPassword!')
superuser.full_clean()
login(page, client, url=live_server.url, user=superuser)
def test_normal_user_create_item(self):
normal_user = get_normal_user()
page.goto(f'{live_server}/admin/')
expect(page).to_have_url(f'{live_server}/admin/')
expect(page).to_have_title(f'Site administration | PyInventory v{__version__}')
context: BrowserContext = self.browser.new_context(
ignore_https_errors=True,
locale='en_US',
timezone_id='Europe/Berlin',
)
with context.new_page() as page, TempImageFile(
format='png', size=(1, 1)
) as png_image, locmem_stats_override_storage() as storage_stats:
login(page, self.client, url=self.live_server_url, user=normal_user)
page.goto(f'{self.live_server_url}/admin/inventory/itemmodel/add/')
expect(page).to_have_title(f'Add Item | PyInventory v{__version__}')
@pytest.mark.playwright
def test_normal_user_create_item(live_server, client, page: Page, png_image):
normal_user = get_normal_user()
login(page, client, url=live_server.url, user=normal_user)
page.locator('label:has-text("Kind:")')
kind_field = page.locator('//input[@id="id_kind"]/..//input[@role="searchbox"]')
kind_field.click()
kind_field.fill('Mainboard')
kind_field.press('Enter')
page.goto(f'{live_server}/admin/inventory/itemmodel/add/')
expect(page).to_have_title(f'Add Item | PyInventory v{__version__}')
page.locator('label:has-text("Producer:")')
producer_field = page.locator('//input[@id="id_producer"]/..//input[@role="searchbox"]')
producer_field.click()
producer_field.fill('Triple D Int.Ltd.')
producer_field.press('Enter')
page.locator('label:has-text("Kind:")')
kind_field = page.locator('//input[@id="id_kind"]/..//input[@role="searchbox"]')
kind_field.click()
kind_field.fill('Mainboard')
kind_field.press('Enter')
page.locator('label:has-text("Name:")')
page.fill('//input[@id="id_name"]', 'TD-20 (8088)')
page.locator('label:has-text("Producer:")')
producer_field = page.locator('//input[@id="id_producer"]/..//input[@role="searchbox"]')
producer_field.click()
producer_field.fill('Triple D Int.Ltd.')
producer_field.press('Enter')
# Add a Link:
page.get_by_role('link', name='Add another Link').click()
page.locator('#id_itemlinkmodel_set-0-url').click()
page.locator('#id_itemlinkmodel_set-0-url').fill('http://test.tld/foo/bar')
page.locator('#id_itemlinkmodel_set-0-name').click()
page.locator('#id_itemlinkmodel_set-0-name').fill('The First Link')
page.locator('#id_itemlinkmodel_set-0-tags').click()
page.locator('#id_itemlinkmodel_set-0-tags').fill('a-link-tag')
page.locator('#id_itemlinkmodel_set-0-tags').press('Tab')
page.locator('label:has-text("Name:")')
page.fill('//input[@id="id_name"]', 'TD-20 (8088)')
# Add Image
page.get_by_role('link', name='Add another Image').click()
page.locator('#id_itemimagemodel_set-0-image').click()
page.locator('#id_itemimagemodel_set-0-image').set_input_files(png_image.name)
page.locator('#id_itemimagemodel_set-0-name').click()
page.locator('#id_itemimagemodel_set-0-name').fill('The Image Name')
page.locator('#id_itemimagemodel_set-0-tags').click()
page.locator('#id_itemimagemodel_set-0-tags').fill('a-image-tag')
page.locator('#id_itemimagemodel_set-0-tags').press('Tab')
# Add a Link:
page.get_by_role('link', name='Add another Link').click()
page.locator('#id_itemlinkmodel_set-0-url').click()
page.locator('#id_itemlinkmodel_set-0-url').fill('http://test.tld/foo/bar')
page.locator('#id_itemlinkmodel_set-0-name').click()
page.locator('#id_itemlinkmodel_set-0-name').fill('The First Link')
page.locator('#id_itemlinkmodel_set-0-tags').click()
page.locator('#id_itemlinkmodel_set-0-tags').fill('a-link-tag')
page.locator('#id_itemlinkmodel_set-0-tags').press('Tab')
assert ItemModel.objects.count() == 0
# Add Image
page.get_by_role('link', name='Add another Image').click()
page.locator('#id_itemimagemodel_set-0-image').click()
page.locator('#id_itemimagemodel_set-0-image').set_input_files(png_image.name)
page.locator('#id_itemimagemodel_set-0-name').click()
page.locator('#id_itemimagemodel_set-0-name').fill('The Image Name')
page.locator('#id_itemimagemodel_set-0-tags').click()
page.locator('#id_itemimagemodel_set-0-tags').fill('a-image-tag')
page.locator('#id_itemimagemodel_set-0-tags').press('Tab')
# Save the item:
page.locator('input:has-text("Save and continue editing")').click()
page.locator('text=The Tunes Item “A Test Tunes Item” was added successfully. You may edit it again')
page.locator('text="Triple D Int.Ltd." - TD-20 (8088)')
assert ItemModel.objects.count() == 0
assert ItemModel.objects.count() == 1
item = ItemModel.objects.first()
assert item.verbose_name() == 'Mainboard - "Triple D Int.Ltd." - TD-20 (8088)'
# Save the item:
page.locator('input:has-text("Save and continue editing")').click()
page.locator('text=The Tunes Item “A Test Tunes Item” was added successfully. You may edit it again')
page.locator('text="Triple D Int.Ltd." - TD-20 (8088)')
# Save & continue?
expect(page).to_have_url(f'{self.live_server_url}/admin/inventory/itemmodel/{item.id}/change/')
assert ItemModel.objects.count() == 1
item = ItemModel.objects.first()
assert item.verbose_name() == 'Mainboard - "Triple D Int.Ltd." - TD-20 (8088)'
# Check added image:
page.locator('text=The Image Name')
img = page.locator('//a[@class="image_file_input_preview"]/img')
img.scroll_into_view_if_needed()
img.is_visible()
assert img.evaluate('image => image.complete') is True
# Save & continue?
expect(page).to_have_url(f'{live_server}/admin/inventory/itemmodel/{item.id}/change/')
assert item.itemimagemodel_set.count() == 1
image: ItemImageModel = item.itemimagemodel_set.first()
assert str(image) == 'The Image Name'
assert image.user == normal_user
assert image.tags.get_tag_list() == ['a-image-tag']
# Check added image:
page.locator('text=The Image Name')
img = page.locator('//a[@class="image_file_input_preview"]/img')
img.scroll_into_view_if_needed()
img.is_visible()
assert img.evaluate('image => image.complete') is True
# Check the added link:
page.locator('text=The First Link')
page.locator('text=Currently: http://test.tld/foo/bar')
links = list(item.itemlinkmodel_set.values_list('name', 'url'))
assert links == [('The First Link', 'http://test.tld/foo/bar')]
link: ItemLinkModel = item.itemlinkmodel_set.first()
assert link.tags.get_tag_list() == ['a-link-tag']
assert item.itemimagemodel_set.count() == 1
image: ItemImageModel = item.itemimagemodel_set.first()
assert str(image) == 'The Image Name'
assert image.user == normal_user
assert image.tags.get_tag_list() == ['a-image-tag']
# Check the added link:
page.locator('text=The First Link')
page.locator('text=Currently: http://test.tld/foo/bar')
links = list(item.itemlinkmodel_set.values_list('name', 'url'))
assert links == [('The First Link', 'http://test.tld/foo/bar')]
link: ItemLinkModel = item.itemlinkmodel_set.first()
assert link.tags.get_tag_list() == ['a-link-tag']
# The "png_image" file should be stored:
self.assertEqual(storage_stats.save_cnt, 1)

Wyświetl plik

@ -1,68 +1,28 @@
import os
import shutil
import subprocess
import sys
from pathlib import Path
from unittest import TestCase
from bx_py_utils.path import assert_is_dir, assert_is_file
from django.conf import settings
from django.core import checks
from django.core.cache import cache
from django.test import TestCase
from django_tools.unittest_utils.project_setup import check_editor_config
from django.core.management import call_command
from manage_django_project.management.commands import code_style
from manageprojects.test_utils.project_setup import check_editor_config, get_py_max_line_length
from packaging.version import Version
import inventory
from inventory_project import PACKAGE_ROOT
from inventory import __version__
from manage import BASE_PATH
def assert_file_contains_string(file_path, string):
with file_path.open('r') as f:
for line in f:
if string in line:
return
raise AssertionError(f'File {file_path} does not contain {string!r} !')
def test_version(package_root=None, version=None):
if package_root is None:
package_root = PACKAGE_ROOT
if version is None:
version = inventory.__version__
ver_obj = Version(inventory.__version__)
if not ver_obj.is_prerelease:
version_string = f'v{version}'
assert_file_contains_string(file_path=Path(package_root, 'README.md'), string=version_string)
assert_file_contains_string(file_path=Path(package_root, 'pyproject.toml'), string=f'version = "{version}"')
def test_poetry_check(package_root=None):
if package_root is None:
package_root = PACKAGE_ROOT
poerty_bin = shutil.which('poetry')
output = subprocess.check_output(
[poerty_bin, 'check'],
text=True,
env=os.environ,
stderr=subprocess.STDOUT,
cwd=str(package_root),
)
print(output)
assert output == 'All set!\n'
class ProjectSettingsTestCase(TestCase):
class ProjectSetupTestCase(TestCase):
def test_project_path(self):
project_path = settings.PROJECT_PATH
assert project_path.is_dir()
assert Path(project_path, 'inventory').is_dir()
assert Path(project_path, 'inventory_project').is_dir()
project_path = settings.BASE_PATH
assert_is_dir(project_path)
assert_is_dir(project_path / 'inventory')
assert_is_dir(project_path / 'inventory_project')
self.assertEqual(project_path, BASE_PATH)
def test_template_dirs(self):
assert len(settings.TEMPLATES) == 1
@ -80,7 +40,6 @@ class ProjectSettingsTestCase(TestCase):
)
all_issue_ids = {issue.id for issue in all_issues}
excpeted_issues = {
'security.W008', # settings.SECURE_SSL_REDIRECT=False
'async.E001', # os.environ['DJANGO_ALLOW_ASYNC_UNSAFE'] exists
}
if all_issue_ids != excpeted_issues:
@ -88,69 +47,56 @@ class ProjectSettingsTestCase(TestCase):
for issue in all_issues:
print(issue)
print('=' * 100)
raise AssertionError('There are check issues!')
raise AssertionError(f'There are check issues (see blow): {all_issue_ids ^ excpeted_issues}')
def test_cache(self):
# django cache should work in tests, because some tests "depends" on it
cache_key = 'a-cache-key'
assert cache.get(cache_key) is None
self.assertIs(cache.get(cache_key), None)
cache.set(cache_key, 'the cache content', timeout=1)
assert cache.get(cache_key) == 'the cache content'
self.assertEqual(cache.get(cache_key), 'the cache content', f'Check: {settings.CACHES=}')
cache.delete(cache_key)
assert cache.get(cache_key) is None
self.assertIs(cache.get(cache_key), None)
def test_settings(self):
assert settings.SETTINGS_MODULE == 'inventory_project.settings.tests'
self.assertEqual(settings.SETTINGS_MODULE, 'inventory_project.settings.tests')
middlewares = [entry.rsplit('.', 1)[-1] for entry in settings.MIDDLEWARE]
assert 'AlwaysLoggedInAsSuperUserMiddleware' not in middlewares
assert 'DebugToolbarMiddleware' not in middlewares
def test_version(self):
self.assertIsNotNone(__version__)
def test_check_editor_config():
check_editor_config(package_root=PACKAGE_ROOT)
version = Version(__version__) # Will raise InvalidVersion() if wrong formatted
self.assertEqual(str(version), __version__)
manage_bin = BASE_PATH / 'manage.py'
assert_is_file(manage_bin)
class CodeStyleTestCase(TestCase):
def call(self, prog, *args):
venv_bin_path = Path(sys.executable).parent
prog = shutil.which(prog, path=venv_bin_path)
assert prog
output = subprocess.check_output([manage_bin, 'version'], text=True)
self.assertIn(__version__, output)
# Darker will call other programs like "flake8", "git"
# Use first our venv bin path:
env_path = f'{venv_bin_path}{os.pathsep}{os.environ["PATH"]}'
def test_manage(self):
manage_bin = BASE_PATH / 'manage.py'
assert_is_file(manage_bin)
return subprocess.check_output(
(prog,) + args,
text=True,
env=dict(PATH=env_path),
stderr=subprocess.STDOUT,
cwd=str(PACKAGE_ROOT),
)
output = subprocess.check_output([manage_bin, 'project_info'], text=True)
self.assertIn('inventory_project', output)
self.assertIn('inventory_project.settings.local', output)
self.assertIn('inventory_project.settings.tests', output)
self.assertIn(__version__, output)
def check_code_style(self):
self.call('darker', '--check')
self.call('isort', '--check-only', '.')
self.call('flake8', '.')
output = subprocess.check_output([manage_bin, 'check'], text=True)
self.assertIn('System check identified no issues (0 silenced).', output)
output = subprocess.check_output([manage_bin, 'makemigrations'], text=True)
self.assertIn("No changes detected", output)
def test_code_style(self):
# lint: ## Run code formatters and linter
# poetry run darker --check
# poetry run isort --check-only .
# poetry run flake8 .
#
# fix-code-style: ## Fix code formatting
# poetry run darker
# poetry run isort .
call_command(code_style.Command())
# First try:
try:
self.check_code_style()
except subprocess.CalledProcessError:
# Fix and test again:
try:
self.call('darker')
self.call('isort', '.')
self.check_code_style() # Check again
except subprocess.CalledProcessError as err:
raise AssertionError(f'Linting error:\n{"-"*100}\n{err.stdout}\n{"-"*100}')
def test_check_editor_config(self):
check_editor_config(package_root=BASE_PATH)
max_line_length = get_py_max_line_length(package_root=BASE_PATH)
self.assertEqual(max_line_length, 119)

115
manage.py 100755
Wyświetl plik

@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
bootstrap CLI
~~~~~~~~~~~~~
Just call this file, and the magic happens ;)
"""
import hashlib
import subprocess
import sys
import venv
from pathlib import Path
def print_no_pip_error():
print('Error: Pip not available!')
print('Hint: "apt-get install python3-venv"\n')
try:
from ensurepip import version
except ModuleNotFoundError as err:
print(err)
print('-' * 100)
print_no_pip_error()
raise
else:
if not version():
print_no_pip_error()
sys.exit(-1)
assert sys.version_info >= (3, 9), 'Python version is too old!'
if sys.platform == 'win32': # wtf
# Files under Windows, e.g.: .../.venv/Scripts/python.exe
BIN_NAME = 'Scripts'
FILE_EXT = '.exe'
else:
# Files under Linux/Mac and all other than Windows, e.g.: .../.venv/bin/python
BIN_NAME = 'bin'
FILE_EXT = ''
BASE_PATH = Path(__file__).parent
VENV_PATH = BASE_PATH / '.venv'
BIN_PATH = VENV_PATH / BIN_NAME
PYTHON_PATH = BIN_PATH / f'python{FILE_EXT}'
PIP_PATH = BIN_PATH / f'pip{FILE_EXT}'
PIP_SYNC_PATH = BIN_PATH / f'pip-sync{FILE_EXT}'
DEP_LOCK_PATH = BASE_PATH / 'requirements.dev.txt'
DEP_HASH_PATH = VENV_PATH / '.dep_hash'
# script file defined in pyproject.toml as [console_scripts]
# (Under Windows: ".exe" not added!)
PROJECT_SHELL_SCRIPT = BIN_PATH / 'inventory_project'
def get_dep_hash():
"""Get SHA512 hash from poetry.lock content."""
return hashlib.sha512(DEP_LOCK_PATH.read_bytes()).hexdigest()
def store_dep_hash():
"""Generate .venv/.dep_hash"""
DEP_HASH_PATH.write_text(get_dep_hash())
def venv_up2date():
"""Is existing .venv is up-to-date?"""
if DEP_HASH_PATH.is_file():
return DEP_HASH_PATH.read_text() == get_dep_hash()
return False
def verbose_check_call(*popen_args):
print(f'\n+ {" ".join(str(arg) for arg in popen_args)}\n')
return subprocess.check_call(popen_args)
def main(argv):
assert DEP_LOCK_PATH.is_file(), f'File not found: "{DEP_LOCK_PATH}" !'
# Create virtual env in ".venv/":
if not PYTHON_PATH.is_file():
print('Create virtual env here:', VENV_PATH.absolute())
builder = venv.EnvBuilder(symlinks=True, upgrade=True, with_pip=True)
builder.create(env_dir=VENV_PATH)
# Update pip
verbose_check_call(PYTHON_PATH, '-m', 'pip', 'install', '-U', 'pip')
if not PIP_SYNC_PATH.is_file():
# Install pip-tools
verbose_check_call(PYTHON_PATH, '-m', 'pip', 'install', '-U', 'pip-tools')
if not PROJECT_SHELL_SCRIPT.is_file() or not venv_up2date():
# install requirements via "pip-sync"
verbose_check_call(PIP_SYNC_PATH, str(DEP_LOCK_PATH))
# install project
verbose_check_call(PIP_PATH, 'install', '--no-deps', '-e', '.')
store_dep_hash()
# Call our entry point CLI:
try:
verbose_check_call(PROJECT_SHELL_SCRIPT, *sys.argv[1:])
except subprocess.CalledProcessError as err:
sys.exit(err.returncode)
if __name__ == '__main__':
main(sys.argv)

2609
poetry.lock wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,113 +1,102 @@
[tool.poetry]
[project]
name = "PyInventory"
version = "0.19.0"
dynamic = ["version"]
description = "Web based management to catalog things including state and location etc. using Python/Django."
license = {text = "GPL-3.0-or-later"}
readme = "README.md"
authors = [
"Jens Diemer <PyInventory@jensdiemer.de>",
]
maintainers = [
"Jens Diemer <PyInventory@jensdiemer.de>",
]
homepage = "https://github.com/jedie/PyInventory"
packages = [
{ include = "inventory" },
{ include = "inventory_project" },
{name = 'Jens Diemer', email = 'PyInventory@jensdiemer.de'}
]
keywords=['inventory','django']
classifiers = [
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3 :: Only",
'Framework :: Django',
"Topic :: Database :: Front-Ends",
"Topic :: Documentation",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Internet :: WWW/HTTP :: Site Management",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
"Operating System :: OS Independent",
requires-python = ">=3.9,<4" # Keep Python 3.9 until Yunohost contains a newer Python Version ;)
dependencies = [
"colorlog", # https://github.com/borntyping/python-colorlog
"gunicorn", # https://github.com/benoimyproject.wsgitc/gunicorn
"django",
"django-import-export", # https://github.com/django-import-export/django-import-export
"django-dbbackup", # https://github.com/django-dbbackup/django-dbbackup
"django-tools", # https://github.com/jedie/django-tools/
"django-reversion-compare", # https://github.com/jedie/django-reversion-compare/
"django-ckeditor", # https://github.com/django-ckeditor/django-ckeditor
"django-tagulous", # https://github.com/radiac/django-tagulous
"django-admin-sortable2", # https://github.com/jrief/django-admin-sortable2
"pillow", # https://github.com/jrief/django-admin-sortable2
"django-debug-toolbar", # http://django-debug-toolbar.readthedocs.io/en/stable/changes.html
"bx_py_utils", # https://github.com/boxine/bx_py_utils
"bx_django_utils", # https://github.com/boxine/bx_django_utils
]
include = ['AUTHORS', 'LICENSE']
license = "GPL-3.0-or-later"
readme = 'README.md'
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/jedie/PyInventory/issues"
[project.optional-dependencies]
dev = [
"manage_django_project>=0.3.0", # https://github.com/jedie/manage_django_project
"cmd2_ext_test", # https://github.com/python-cmd2/cmd2/tree/master/plugins/ext_test
"playwright", # https://playwright.dev/python/docs/intro
"beautifulsoup4",
"tblib", # https://github.com/ionelmc/python-tblib
"pip-tools", # https://github.com/jazzband/pip-tools/
"tox", # https://github.com/tox-dev/tox
"coverage", # https://github.com/nedbat/coveragepy
"autopep8", # https://github.com/hhatto/autopep8
"pyupgrade", # https://github.com/asottile/pyupgrade
"flake8", # https://github.com/pycqa/flake8
"pyflakes", # https://github.com/PyCQA/pyflakes
"codespell", # https://github.com/codespell-project/codespell
"EditorConfig", # https://github.com/editorconfig/editorconfig-core-py
"safety", # https://github.com/pyupio/safety
"mypy", # https://github.com/python/mypy
"twine", # https://github.com/pypa/twine
# https://github.com/akaihola/darker
# https://github.com/ikamensh/flynt
# https://github.com/pycqa/isort
# https://github.com/pygments/pygments
"darker[flynt, isort, color]",
"tomli", # https://github.com/hukkin/tomli
# tomli only needed for Python <3.11, but see bug:
# https://github.com/pypa/pip/issues/9644#issuecomment-1456583402
#"tomli;python_version<\"3.11\"", # https://github.com/hukkin/tomli
# Work-a-round for:
# https://github.com/jazzband/pip-tools/issues/994#issuecomment-1321226661
"typing-extensions>=3.10",
"model_bakery", # https://github.com/model-bakers/model_bakery
"requests-mock",
"django-override-storage", # https://github.com/danifus/django-override-storage
]
[project.urls]
Documentation = "https://github.com/jedie/PyInventory"
Source = "https://github.com/jedie/PyInventory"
[tool.poetry.dependencies]
python = ">=3.9,<4.0.0" # Stay with 3.9 until YunoHost used >=Debian 11 (Bullseye)
django = "*"
colorlog = "*" # https://github.com/borntyping/python-colorlog
gunicorn = "*" # https://github.com/benoimyproject.wsgitc/gunicorn
django-debug-toolbar = "*" # http://django-debug-toolbar.readthedocs.io/en/stable/changes.html
django-import-export = "*" # https://github.com/django-import-export/django-import-export
django-dbbackup = "*" # https://github.com/django-dbbackup/django-dbbackup
django-tools = "*" # https://github.com/jedie/django-tools/
django-reversion-compare = "*" # https://github.com/jedie/django-reversion-compare/
django-ckeditor = "*" # https://github.com/django-ckeditor/django-ckeditor
bx_py_utils = "*" # https://github.com/boxine/bx_py_utils
bx_django_utils = "*" # https://github.com/boxine/bx_django_utils
django-tagulous = "*" # https://github.com/radiac/django-tagulous
django-admin-sortable2 = "*" # https://github.com/jrief/django-admin-sortable2
django-axes = "*" # https://github.com/jazzband/django-axes
requests = "*" # https://github.com/psf/requests
pillow = "*"
[project.scripts]
inventory_project = "inventory_project.__main__:main"
psycopg2-binary = { version = "*", optional = true } # install via: poetry install --extras "postgres-binary"
psycopg2 = { version = "*", optional = true } # install via: poetry install --extras "psycopg2-source"
[manage_django_project]
module_name="inventory_project"
# Django settings used for all commands except test/coverage/tox:
local_settings='inventory_project.settings.local'
[tool.poetry.extras]
postgres-binary = ["psycopg2-binary"]
psycopg2-source = ["psycopg2"]
[tool.poetry.dev-dependencies]
dev_shell = "*" # https://github.com/jedie/dev-shell
cmd2_ext_test = "*"
pytest = "*"
pytest-randomly = "*"
pytest-cov = "*"
pytest-django = "*"
pytest-playwright = "*" # https://playwright.dev/python/docs/test-runners
pyupgrade = "*"
model_bakery = "*" # https://github.com/model-bakers/model_bakery
beautifulsoup4 = "*"
lxml = "*"
requests-mock = "*"
tox = "*" # https://github.com/tox-dev/tox
coveralls = "*" # http://github.com/TheKevJames/coveralls-python
flake8 = "*" # https://github.com/pycqa/flake8
EditorConfig = "*" # https://github.com/editorconfig/editorconfig-core-py
safety = "*" # https://github.com/pyupio/safety
mypy = "*" # https://github.com/python/mypy
twine = "*" # https://github.com/pypa/twine
poetry-publish = "*" # https://github.com/jedie/poetry-publish
# https://github.com/akaihola/darker
# https://github.com/ikamensh/flynt
# https://github.com/pycqa/isort
# https://github.com/pygments/pygments
darker = { version = "*", extras = ["flynt", "isort", "color"]}
tomli = "*" # https://github.com/hukkin/tomli
# tomli only needed for Python <3.11, but see bug:
# https://github.com/pypa/pip/issues/9644#issuecomment-1456583402
#tomli = {version = "*", markers = "python_version < \"3.11\""} # https://github.com/hukkin/tomli
[tool.poetry.scripts]
devshell = 'inventory_project.dev_shell:devshell_cmdloop'
run_testserver = 'inventory_project.manage:start_test_server'
# Django settings used for test/coverage/tox commands:
test_settings='inventory_project.settings.tests'
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["setuptools>=61.0", "setuptools_scm>=7.1"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["."]
include = ["inventory*", "inventory_project*"]
[tool.setuptools.dynamic]
version = {attr = "inventory.__version__"}
[tool.darker]
@ -132,21 +121,21 @@ log_level = "INFO"
# https://pycqa.github.io/isort/docs/configuration/config_files/#pyprojecttoml-preferred-format
atomic=true
profile='black'
skip_glob=[".*", "*/htmlcov/*","*/migrations/*","*/volumes/*"]
known_first_party=["inventory","inventory_project","inventory_tests"]
skip_glob=['.*', '*/htmlcov/*','*/migrations/*']
known_first_party=['inventory']
line_length=119
lines_after_imports=2
[tool.coverage.run]
branch = false # coverage.exceptions.DataError: Can't combine arc data with line data
branch = true
parallel = true
source = ['.']
concurrency = ["multiprocessing"]
command_line = "-m unittest --locals --verbose"
source = ['.']
command_line = '-m inventory_project test --shuffle --parallel --buffer'
[tool.coverage.report]
omit = ['.*', '*/tests/*']
omit = ['.*', '*/tests/*', '*/migrations/*']
skip_empty = true
fail_under = 30
show_missing = true
@ -158,45 +147,6 @@ exclude_lines = [
]
[tool.pytest.ini_options]
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml
minversion = "6.0"
DJANGO_SETTINGS_MODULE="inventory_project.settings.tests"
# Don't overwrite settings.DEBUG:
django_debug_mode="keep"
markers = [
"playwright: marks Playwright tests",
]
norecursedirs = ".* .git __pycache__ coverage* dist htmlcov volumes"
# sometimes helpfull "addopts" arguments:
# -vv
# --verbose
# --capture=no
# --trace-config
# --full-trace
# -p no:warnings
# -m "not playwright"
addopts = """
--ignore-glob=deployment/django/*
--reuse-db
--nomigrations
--cov=.
--cov-report term-missing
--cov-report xml
--no-cov-on-fail
--showlocals
--doctest-modules
--failed-first
--last-failed-no-failures all
--new-first
-p no:randomly
"""
# TODO: --mypy
[tool.tox] # https://tox.wiki/en/latest/config.html#pyproject-toml
legacy_tox_ini = """
[tox]
@ -206,11 +156,12 @@ skip_missing_interpreters = True
[testenv]
passenv = *
allowlist_externals = poetry
skip_install = true
commands_pre =
pip install -U pip-tools
pip-sync requirements.dev.txt
commands =
python --version
poetry run django-admin --version
python devshell.py pytest
{envpython} -m coverage run --context='{envname}' -m inventory_project test --buffer --shuffle
"""
@ -220,23 +171,20 @@ ignore_missing_imports = true
allow_redefinition = true # https://github.com/python/mypy/issues/7165
show_error_codes = true
plugins = []
exclude = ['.venv', 'tests']
exclude = ['.venv', 'tests', 'migrations']
[manageprojects] # https://github.com/jedie/manageprojects
initial_revision = "b33d693"
initial_date = 2022-12-21T22:53:08+01:00
initial_revision = "7cece02"
initial_date = 2023-07-15T16:37:28+02:00
cookiecutter_template = "https://github.com/jedie/cookiecutter_templates/"
cookiecutter_directory = "poetry-python"
applied_migrations = [
"183124a", # 2023-04-04T12:26:15+02:00
]
cookiecutter_directory = "managed-django-project"
[manageprojects.cookiecutter_context.cookiecutter]
full_name = "Jens Diemer"
github_username = "jedie"
author_email = "PyInventory@jensdiemer.de"
package_name = "PyInventory"
package_name = "inventory"
package_version = "0.18.0"
package_description = "Web based management to catalog things including state and location etc. using Python/Django."
package_url = "https://github.com/jedie/PyInventory"

1116
requirements.dev.txt 100644

Plik diff jest za duży Load Diff

262
requirements.txt 100644
Wyświetl plik

@ -0,0 +1,262 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# ./manage.py update_req
#
asgiref==3.7.2 \
--hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \
--hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed
# via django
bleach==6.0.0 \
--hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
--hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
# via django-tools
bx-django-utils==63 \
--hash=sha256:0023c0c18c8ce21fbee0e3bb563cd0283749495ca22cab1857ac971e4ee2bb05 \
--hash=sha256:3b050d9d9d4e496e082c29d98d7633eb89ad028c658743b0032ee88e7e49be63
# via PyInventory (pyproject.toml)
bx-py-utils==85 \
--hash=sha256:8d6ee4bb0c431304b812f5bebb1bc8e2ab05f1b6c2f8d16d352cbcee5e916cd2 \
--hash=sha256:df023fa05cda8e969d2cbdb4cc348d8b7670567a2fe775faf7a0c869ec56eaa2
# via
# PyInventory (pyproject.toml)
# bx-django-utils
# django-tools
colorlog==6.7.0 \
--hash=sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662 \
--hash=sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5
# via PyInventory (pyproject.toml)
defusedxml==0.7.1 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
# via odfpy
diff-match-patch==20230430 \
--hash=sha256:953019cdb9c9d2c9e47b5b12bcff3cf4746fc4598eb406076fa1fc27e6a1f15c \
--hash=sha256:dce43505fb7b1b317de7195579388df0746d90db07015ed47a85e5e44930ef93
# via
# django-import-export
# django-reversion-compare
django==4.2.3 \
--hash=sha256:45a747e1c5b3d6df1b141b1481e193b033fd1fdbda3ff52677dc81afdaacbaed \
--hash=sha256:f7c7852a5ac5a3da5a8d5b35cc6168f31b605971441798dac845f17ca8028039
# via
# PyInventory (pyproject.toml)
# bx-django-utils
# django-admin-sortable2
# django-ckeditor
# django-dbbackup
# django-debug-toolbar
# django-import-export
# django-js-asset
# django-reversion
# django-reversion-compare
# django-tagulous
# django-tools
django-admin-sortable2==2.1.9 \
--hash=sha256:6de19689cb2f131d256ce19d2fd148728d551943d8463b1d81f6334adfa0b6fc \
--hash=sha256:bf036785c598685a0019eb08340b88fe6ca74bd178033e2290e6c41b62fa4bf1
# via PyInventory (pyproject.toml)
django-ckeditor==6.6.1 \
--hash=sha256:44894c2a1050f58edcab6f9e631dbb63fa5532e7432a5650cfd878b6f33b0d46 \
--hash=sha256:a804a98137058c00fc092aafc475e9491b2ca7c68c84d6ca12c4fa2453696017
# via PyInventory (pyproject.toml)
django-dbbackup==4.0.2 \
--hash=sha256:1874d684abc22260972a67668a6db3331b24d7e1e8af89eaffdcd61eb27dbc2a \
--hash=sha256:3ccde831f1a8268fb031b37a8e7e2de3abb556623023af1e859cd7104c09ea2a
# via PyInventory (pyproject.toml)
django-debug-toolbar==4.1.0 \
--hash=sha256:a0b532ef5d52544fd745d1dcfc0557fa75f6f0d1962a8298bd568427ef2fa436 \
--hash=sha256:f57882e335593cb8e74c2bda9f1116bbb9ca8fc0d81b50a75ace0f83de5173c7
# via PyInventory (pyproject.toml)
django-import-export==3.2.0 \
--hash=sha256:1d3f2cb2ee3cca0386ed60651fa1623be989f130d9fbdf98a67f7dc3a94b8a37 \
--hash=sha256:38fd7b9439b9e3aa1a4747421c1087a5bc194e915a28d795fb8429a5f8028f2d
# via PyInventory (pyproject.toml)
django-js-asset==2.1.0 \
--hash=sha256:36a3a4dd6e9efc895fb127d13126020f6ec1ec9469ad42878d42143f22495d90 \
--hash=sha256:be6f69ae5c4865617aa7726c48eddb64089a1e7d4ea7d22a35a3beb8282020f6
# via django-ckeditor
django-reversion==5.0.4 \
--hash=sha256:a591cbce8621b5d036a37617554668b5ef2eb9777682e3af20b6401ee87cfbc5 \
--hash=sha256:c12bab452d31dd3c244456cf1df383acf14ba147cf99404c5e44412596de42fc
# via django-reversion-compare
django-reversion-compare==0.16.2 \
--hash=sha256:5629f226fc73bd7b95de47b2e21e2eba2fa39f004ba0fee6d460e96676c0dc9b \
--hash=sha256:9d7d096534f5d0e49d7419a8a29b4517580e6a7855529e594d10bfb373f980ab
# via PyInventory (pyproject.toml)
django-tagulous==1.3.3 \
--hash=sha256:ad3bb85f4cce83a47e4c0257143229cb92a294defa02fe661823b0442b35d478 \
--hash=sha256:d445590ae1b5cb9b8c5a425f97bf5f01148a33419c19edeb721ebd9fdd6792fe
# via PyInventory (pyproject.toml)
django-tools==0.54.0 \
--hash=sha256:5040a91282be9d1c9d379b0c65da50bcb3691bff03cee54fd4123ace238c3a43 \
--hash=sha256:a7b7bfa5b9c5a81966454d17dffb2403cee25a806c858ee0486a08798227598f
# via PyInventory (pyproject.toml)
et-xmlfile==1.1.0 \
--hash=sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c \
--hash=sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada
# via openpyxl
gunicorn==21.2.0 \
--hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \
--hash=sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033
# via PyInventory (pyproject.toml)
icdiff==2.0.6 \
--hash=sha256:a2673b335d671e64fc73c44e1eaa0aa01fd0e68354e58ee17e863ab29912a79a
# via django-tools
markuppy==1.14 \
--hash=sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f
# via tablib
odfpy==1.4.1 \
--hash=sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec
# via tablib
openpyxl==3.1.2 \
--hash=sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184 \
--hash=sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5
# via tablib
packaging==23.1 \
--hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
--hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
# via gunicorn
pillow==10.0.0 \
--hash=sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5 \
--hash=sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530 \
--hash=sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d \
--hash=sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca \
--hash=sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891 \
--hash=sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992 \
--hash=sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7 \
--hash=sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3 \
--hash=sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba \
--hash=sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3 \
--hash=sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3 \
--hash=sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f \
--hash=sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538 \
--hash=sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3 \
--hash=sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d \
--hash=sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c \
--hash=sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017 \
--hash=sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3 \
--hash=sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223 \
--hash=sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e \
--hash=sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3 \
--hash=sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6 \
--hash=sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640 \
--hash=sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334 \
--hash=sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1 \
--hash=sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba \
--hash=sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa \
--hash=sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0 \
--hash=sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396 \
--hash=sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d \
--hash=sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485 \
--hash=sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf \
--hash=sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43 \
--hash=sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37 \
--hash=sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2 \
--hash=sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd \
--hash=sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86 \
--hash=sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967 \
--hash=sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629 \
--hash=sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568 \
--hash=sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed \
--hash=sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f \
--hash=sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551 \
--hash=sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3 \
--hash=sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614 \
--hash=sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff \
--hash=sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d \
--hash=sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883 \
--hash=sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684 \
--hash=sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0 \
--hash=sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de \
--hash=sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b \
--hash=sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3 \
--hash=sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199 \
--hash=sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51 \
--hash=sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90
# via PyInventory (pyproject.toml)
pprintpp==0.4.0 \
--hash=sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d \
--hash=sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403
# via django-tools
python-stdnum==1.18 \
--hash=sha256:bcc763d9c49ae23da5d2b7a686d5fd1deec9d9051341160a10d1ac723a26bec0 \
--hash=sha256:d7f2a3c7ef4635c957b9cbdd9b1993d1f6ee3a2959f03e172c45440d99f296eb
# via bx-django-utils
pytz==2023.3 \
--hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
--hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
# via django-dbbackup
pyyaml==6.0.1 \
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
--hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
--hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
--hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
--hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
--hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
--hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
--hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
--hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
--hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
--hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
--hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
--hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
--hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
--hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
--hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
--hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
--hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
--hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
--hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
--hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
--hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
--hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
--hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
--hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
--hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
--hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
--hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
--hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
--hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
--hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
# via tablib
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via bleach
sqlparse==0.4.4 \
--hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \
--hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c
# via
# django
# django-debug-toolbar
tablib[html,ods,xls,xlsx,yaml]==3.5.0 \
--hash=sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9 \
--hash=sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33
# via django-import-export
typing-extensions==4.7.1 \
--hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \
--hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2
# via asgiref
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
# via bleach
xlrd==2.0.1 \
--hash=sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd \
--hash=sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88
# via tablib
xlwt==1.3.0 \
--hash=sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e \
--hash=sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88
# via tablib