diff --git a/.env.example b/.env.example index 5c4c0d97..da4d2ac8 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,10 @@ SECRET_KEY=(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2 DATABASE_URL=sqlite:///db.sqlite3 #DATABASE_URL=postgres://user:password@127.0.0.1:5432/sith -CACHE_URL=redis://127.0.0.1:6379/0 +REDIS_PORT=7963 +CACHE_URL=redis://127.0.0.1:${REDIS_PORT}/0 + +# Used to select which other services to run alongside +# runserver and pytest +PROCFILE_RUNSERVER=Procfile.dev +PROCFILE_PYTEST=Procfile.pytest diff --git a/.gitignore b/.gitignore index 19b65265..eb78b41d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ node_modules/ # compiled documentation site/ .env + +### Redis ### + +# Ignore redis binary dump (dump.rdb) files + +*.rdb diff --git a/Procfile b/Procfile deleted file mode 100644 index f3ce1a69..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -bundler: npm run $BUNDLER_MODE \ No newline at end of file diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 00000000..31652ef9 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +bundler: npm run serve +redis: redis-server --port $REDIS_PORT \ No newline at end of file diff --git a/Procfile.pytest b/Procfile.pytest new file mode 100644 index 00000000..4f9c4808 --- /dev/null +++ b/Procfile.pytest @@ -0,0 +1 @@ +redis: redis-server --port $REDIS_PORT diff --git a/manage.py b/manage.py index 56271706..71939ec3 100755 --- a/manage.py +++ b/manage.py @@ -13,13 +13,25 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # - import os import sys +from django.utils.autoreload import DJANGO_AUTORELOAD_ENV + +from processes.composer import start_composer, stop_composer +from sith.environ import env + if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sith.settings") from django.core.management import execute_from_command_line + if ( + os.environ.get(DJANGO_AUTORELOAD_ENV) is None + and (procfile := env.str("PROCFILE_RUNSERVER", None)) is not None + ): + start_composer(procfile) + execute_from_command_line(sys.argv) + + stop_composer() diff --git a/processes/composer.py b/processes/composer.py new file mode 100644 index 00000000..8355e205 --- /dev/null +++ b/processes/composer.py @@ -0,0 +1,26 @@ +import os +import signal +import subprocess +import sys + +import psutil + +COMPOSER_PID = "COMPOSER_PID" + + +def start_composer(procfile: str): + """Starts the composer and stores the PID as an environment variable + This allows for running smoothly with the django reloader + """ + process = subprocess.Popen( + [sys.executable, "-m", "honcho", "-f", procfile, "start"], + ) + os.environ[COMPOSER_PID] = str(process.pid) + + +def stop_composer(): + """Stops the composer if it was started before""" + if (pid := os.environ.get(COMPOSER_PID, None)) is not None: + process = psutil.Process(int(pid)) + process.send_signal(signal.SIGTERM) + process.wait() diff --git a/pyproject.toml b/pyproject.toml index cf09d61f..d5d3c1f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dependencies = [ "environs[django]<15.0.0,>=14.1.0", "requests>=2.32.3", "honcho>=2.0.0", + "psutil>=7.0.0", ] [project.urls] @@ -125,6 +126,9 @@ ignore = [ [tool.ruff.lint.pydocstyle] convention = "google" +[project.entry-points.pytest11] +sith = "sith.pytest" + [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "sith.settings" python_files = ["tests.py", "test_*.py", "*_tests.py"] diff --git a/sith/environ.py b/sith/environ.py new file mode 100644 index 00000000..0b048822 --- /dev/null +++ b/sith/environ.py @@ -0,0 +1,4 @@ +from environs import Env + +env = Env() +_ = env.read_env() diff --git a/sith/pytest.py b/sith/pytest.py new file mode 100644 index 00000000..3a99a7fc --- /dev/null +++ b/sith/pytest.py @@ -0,0 +1,23 @@ +import pytest + +from processes.composer import start_composer, stop_composer + +from .environ import env + +# pytest-django uses the load_initial_conftest hook +# it's the first hook loaded by pytest and can only +# be defined in a proper pytest plugin +# To use the composer before pytest-django loads +# we need to define this hook and thus create a proper +# pytest plugin. We can't just use conftest.py + + +@pytest.hookimpl(tryfirst=True) +def pytest_load_initial_conftests(early_config, parser, args): + """Hook that loads the composer before the pytest-django plugin""" + if (procfile := env.str("PROCFILE_PYTEST", None)) is not None: + start_composer(procfile) + + +def pytest_unconfigure(config): + stop_composer() diff --git a/sith/settings.py b/sith/settings.py index 8191251f..22ccf09e 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -42,14 +42,11 @@ from pathlib import Path import sentry_sdk from dateutil.relativedelta import relativedelta from django.utils.translation import gettext_lazy as _ -from environs import Env from sentry_sdk.integrations.django import DjangoIntegration +from .environ import env from .honeypot import custom_honeypot_error -env = Env() -env.read_env() - BASE_DIR = Path(__file__).parent.parent.resolve() # Quick-start development settings - unsuitable for production diff --git a/staticfiles/management/commands/runserver.py b/staticfiles/management/commands/runserver.py index 5fc80a64..8dd88538 100644 --- a/staticfiles/management/commands/runserver.py +++ b/staticfiles/management/commands/runserver.py @@ -1,13 +1,6 @@ -import logging -import os -import subprocess -import sys - -from django.conf import settings from django.contrib.staticfiles.management.commands.runserver import ( Command as Runserver, ) -from django.utils.autoreload import DJANGO_AUTORELOAD_ENV from staticfiles.processors import OpenApi @@ -16,19 +9,5 @@ class Command(Runserver): """Light wrapper around default runserver that integrates javascirpt auto bundling.""" def run(self, **options): - # OpenApi generation needs to be before the bundler OpenApi.compile() - # Run all other web processes but only if debug mode is enabled - # Also protects from re-launching the server if django reloads it - if os.environ.get(DJANGO_AUTORELOAD_ENV) is None: - logging.getLogger("django").info("Running complementary processes") - with subprocess.Popen( - [sys.executable, "-m", "honcho", "start"], - env={ - **os.environ, - **{"BUNDLER_MODE": "serve" if settings.DEBUG else "compile"}, - }, - ): - super().run(**options) - return super().run(**options) diff --git a/uv.lock b/uv.lock index 69a94a95..11b00547 100644 --- a/uv.lock +++ b/uv.lock @@ -1104,6 +1104,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, ] +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + [[package]] name = "psycopg" version = "3.2.3" @@ -1520,6 +1535,7 @@ dependencies = [ { name = "mistune" }, { name = "phonenumbers" }, { name = "pillow" }, + { name = "psutil" }, { name = "pydantic-extra-types" }, { name = "python-dateutil" }, { name = "redis", extra = ["hiredis"] }, @@ -1581,6 +1597,7 @@ requires-dist = [ { name = "mistune", specifier = ">=3.0.2,<4.0.0" }, { name = "phonenumbers", specifier = ">=8.13.52,<9.0.0" }, { name = "pillow", specifier = ">=11.0.0,<12.0.0" }, + { name = "psutil", specifier = ">=7.0.0" }, { name = "pydantic-extra-types", specifier = ">=2.10.1,<3.0.0" }, { name = "python-dateutil", specifier = ">=2.9.0.post0,<3.0.0.0" }, { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0,<6.0.0" },