kopia lustrzana https://github.com/jedie/PyInventory
Migrate from "poetry-python" to "managed-django-project"
* Remove poetry, pytest and devshell * Use pip-tools, unittests and https://github.com/jedie/manage_django_projectpull/141/head
rodzic
0b38878144
commit
c6303556a9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
149
devshell.py
149
devshell.py
|
@ -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)
|
|
@ -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'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
from django_tools.management.commands.run_testserver import Command as RunServerCommand
|
||||
|
||||
|
||||
class Command(RunServerCommand):
|
||||
pass
|
|
@ -7,3 +7,6 @@ import inventory
|
|||
|
||||
PACKAGE_ROOT = Path(inventory.__file__).parent.parent
|
||||
assert_is_dir(PACKAGE_ROOT / 'inventory')
|
||||
|
||||
|
||||
__version__ = inventory.__version__
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
},
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!')
|
|
@ -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
|
|
@ -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
|
|
@ -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__}')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
Plik diff jest za duży
Load Diff
254
pyproject.toml
254
pyproject.toml
|
@ -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"
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -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
|
Ładowanie…
Reference in New Issue