From 8528820d89fde995bc8d59481fef6302f441f70c Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 15 Feb 2025 12:15:08 +0100 Subject: [PATCH 01/11] Run bundler through honcho --- Procfile | 1 + pyproject.toml | 1 + staticfiles/management/commands/runserver.py | 10 +++++++--- staticfiles/processors.py | 6 ------ uv.lock | 18 ++++++++++++++++-- 5 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..857b2f2d --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +bundler: npm run serve \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a4d16abc..cf09d61f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "redis[hiredis]<6.0.0,>=5.2.0", "environs[django]<15.0.0,>=14.1.0", "requests>=2.32.3", + "honcho>=2.0.0", ] [project.urls] diff --git a/staticfiles/management/commands/runserver.py b/staticfiles/management/commands/runserver.py index f093f3bd..8614d8b9 100644 --- a/staticfiles/management/commands/runserver.py +++ b/staticfiles/management/commands/runserver.py @@ -1,4 +1,7 @@ +import logging import os +import subprocess +import sys from django.conf import settings from django.contrib.staticfiles.management.commands.runserver import ( @@ -6,7 +9,7 @@ from django.contrib.staticfiles.management.commands.runserver import ( ) from django.utils.autoreload import DJANGO_AUTORELOAD_ENV -from staticfiles.processors import JSBundler, OpenApi +from staticfiles.processors import OpenApi class Command(Runserver): @@ -15,10 +18,11 @@ class Command(Runserver): def run(self, **options): # OpenApi generation needs to be before the bundler OpenApi.compile() - # Only run the bundling server when debug is enabled + # 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 and settings.DEBUG: - with JSBundler.runserver(): + logging.getLogger("django").info("Running complementary processes") + with subprocess.Popen([sys.executable, "-m", "honcho", "start"]): super().run(**options) return super().run(**options) diff --git a/staticfiles/processors.py b/staticfiles/processors.py index 3a0df243..bacac363 100644 --- a/staticfiles/processors.py +++ b/staticfiles/processors.py @@ -99,12 +99,6 @@ class JSBundler: if process.returncode: raise RuntimeError(f"Bundler failed with returncode {process.returncode}") - @staticmethod - def runserver() -> subprocess.Popen: - """Bundle js files automatically in background when called in debug mode.""" - logging.getLogger("django").info("Running javascript bundling server") - return subprocess.Popen(["npm", "run", "serve"]) - @staticmethod def get_manifest() -> JSBundlerManifest: return JSBundlerManifest(BUNDLED_ROOT / ".vite" / "manifest.json") diff --git a/uv.lock b/uv.lock index c76028d9..69a94a95 100644 --- a/uv.lock +++ b/uv.lock @@ -155,7 +155,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -595,6 +595,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/04/eaa88433249ddfc282018d3da4198d0b0018e48768e137bfad304aacb1ec/hiredis-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9020fd7e58f489fda6a928c31355add0e665fd6b87b21954e675cf9943eafa32", size = 22004 }, ] +[[package]] +name = "honcho" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/c8/d860888358bf5c8a6e7d78d1b508b59b0e255afd5655f243b8f65166dafd/honcho-2.0.0.tar.gz", hash = "sha256:af3815c03c634bf67d50f114253ea9fef72ecff26e4fd06b29234789ac5b8b2e", size = 45618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/1c/25631fc359955569e63f5446dbb7022c320edf9846cbe892ee5113433a7e/honcho-2.0.0-py3-none-any.whl", hash = "sha256:56dcd04fc72d362a4befb9303b1a1a812cba5da283526fbc6509be122918ddf3", size = 22093 }, +] + [[package]] name = "ical" version = "8.3.1" @@ -807,7 +819,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -1501,6 +1513,7 @@ dependencies = [ { name = "django-phonenumber-field" }, { name = "django-simple-captcha" }, { name = "environs", extra = ["django"] }, + { name = "honcho" }, { name = "ical" }, { name = "jinja2" }, { name = "libsass" }, @@ -1561,6 +1574,7 @@ requires-dist = [ { name = "django-phonenumber-field", specifier = ">=8.0.0,<9.0.0" }, { name = "django-simple-captcha", specifier = ">=0.6.0,<1.0.0" }, { name = "environs", extras = ["django"], specifier = ">=14.1.0,<15.0.0" }, + { name = "honcho", specifier = ">=2.0.0" }, { name = "ical", specifier = ">=8.3.0,<9.0.0" }, { name = "jinja2", specifier = ">=3.1.4,<4.0.0" }, { name = "libsass", specifier = ">=0.23.0,<1.0.0" }, From 6841d96455f993e2848422f7eb86b7437da6b1b5 Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 15 Feb 2025 17:58:07 +0100 Subject: [PATCH 02/11] Enable honcho on non debug --- Procfile | 2 +- docs/tutorial/structure.md | 6 ++++-- staticfiles/management/commands/runserver.py | 10 ++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Procfile b/Procfile index 857b2f2d..f3ce1a69 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -bundler: npm run serve \ No newline at end of file +bundler: npm run $BUNDLER_MODE \ No newline at end of file diff --git a/docs/tutorial/structure.md b/docs/tutorial/structure.md index aff331d2..4ba0077e 100644 --- a/docs/tutorial/structure.md +++ b/docs/tutorial/structure.md @@ -80,6 +80,7 @@ sith/ ├── pyproject.toml (30) ├── .venv/ (31) ├── .python-version (32) +├── Procfile (33) └── README.md ``` @@ -136,7 +137,8 @@ sith/ de certaines d'entre elles. 31. Dossier d'environnement virtuel généré par uv 32. Fichier qui contrôle quelle version de python utiliser pour le projet - +33. Fichier qui contrôle les services additionnels à lancer + avec le serveur de développement ## L'application principale @@ -220,4 +222,4 @@ comme suit : L'organisation peut éventuellement être un peu différente pour certaines applications, mais le principe -général est le même. \ No newline at end of file +général est le même. diff --git a/staticfiles/management/commands/runserver.py b/staticfiles/management/commands/runserver.py index 8614d8b9..5fc80a64 100644 --- a/staticfiles/management/commands/runserver.py +++ b/staticfiles/management/commands/runserver.py @@ -20,9 +20,15 @@ class Command(Runserver): 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 and settings.DEBUG: + if os.environ.get(DJANGO_AUTORELOAD_ENV) is None: logging.getLogger("django").info("Running complementary processes") - with subprocess.Popen([sys.executable, "-m", "honcho", "start"]): + 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) From ba6e2a6402c8bd53873a30b4d6a3dd51297f3061 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 17 Feb 2025 15:58:53 +0100 Subject: [PATCH 03/11] Integrate automatic redis startup with the project --- .env.example | 8 +++++- .gitignore | 6 +++++ Procfile | 1 - Procfile.dev | 2 ++ Procfile.pytest | 1 + manage.py | 14 ++++++++++- processes/composer.py | 26 ++++++++++++++++++++ pyproject.toml | 4 +++ sith/environ.py | 4 +++ sith/pytest.py | 23 +++++++++++++++++ sith/settings.py | 5 +--- staticfiles/management/commands/runserver.py | 21 ---------------- uv.lock | 17 +++++++++++++ 13 files changed, 104 insertions(+), 28 deletions(-) delete mode 100644 Procfile create mode 100644 Procfile.dev create mode 100644 Procfile.pytest create mode 100644 processes/composer.py create mode 100644 sith/environ.py create mode 100644 sith/pytest.py 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" }, From 3b80b36ed6acb536dcc1a614604dfe03b4dd8309 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 17 Feb 2025 16:20:33 +0100 Subject: [PATCH 04/11] Update doc --- docs/tutorial/install-advanced.md | 24 ++++++++++++++++ docs/tutorial/install.md | 13 ++++----- docs/tutorial/structure.md | 47 ++++++++++++++++++------------- sith/settings.py | 3 +- 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/docs/tutorial/install-advanced.md b/docs/tutorial/install-advanced.md index 7b4fe493..a46cbd6e 100644 --- a/docs/tutorial/install-advanced.md +++ b/docs/tutorial/install-advanced.md @@ -77,6 +77,30 @@ uv sync --group prod C'est parce que ces dépendances compilent certains modules à l'installation. +## Configurer Redis + +Redis est installé comme dépendance mais pas lancé par défaut. + +En mode développement, le sith se charge de le démarrer mais +pas en production ! + +Il faut donc lancer le service comme ceci: + +```bash +sudo systemctl start redis +sudo systemctl enable redis # si vous voulez que redis démarre automatiquement au boot +``` + +Puis modifiez votre `.env` pour y configurer le bon port redis. +Le port du fichier d'exemple est un port non standard pour éviter +les conflits avec les instances de redis déjà en fonctionnement. + +```dotenv +REDIS_PORT=6379 +CACHE_URL=redis://127.0.0.1:${REDIS_PORT}/0 +``` + + ## Configurer PostgreSQL PostgreSQL est utilisé comme base de données. diff --git a/docs/tutorial/install.md b/docs/tutorial/install.md index 0a621587..a4635c02 100644 --- a/docs/tutorial/install.md +++ b/docs/tutorial/install.md @@ -100,14 +100,6 @@ cd /mnt//vos/fichiers/comme/dhab Python ne fait pas parti des dépendances puisqu'il est automatiquement installé par uv. -Parmi les dépendances installées se trouve redis (que nous utilisons comme cache). -Redis est un service qui doit être activé pour être utilisé. -Pour cela, effectuez les commandes : - -```bash -sudo systemctl start redis -sudo systemctl enable redis # si vous voulez que redis démarre automatiquement au boot -``` ## Finaliser l'installation @@ -179,6 +171,11 @@ uv run ./manage.py runserver [http://localhost:8000](http://localhost:8000) ou bien [http://127.0.0.1:8000/](http://127.0.0.1:8000/). +!!!note + + Le serveur de développement se charge de lancer redis + et les autres services nécessaires au bon fonctionnement du site. + !!!tip Vous trouverez également, à l'adresse diff --git a/docs/tutorial/structure.md b/docs/tutorial/structure.md index 4ba0077e..6ab36c6c 100644 --- a/docs/tutorial/structure.md +++ b/docs/tutorial/structure.md @@ -66,21 +66,24 @@ sith/ │ └── ... ├── staticfiles/ (23) │ └── ... +├── processes/ (24) +│ └── ... │ -├── .coveragerc (24) -├── .envrc (25) +├── .coveragerc (25) +├── .envrc (26) ├── .gitattributes ├── .gitignore ├── .mailmap -├── .env (26) -├── .env.example (27) -├── manage.py (28) -├── mkdocs.yml (29) +├── .env (27) +├── .env.example (28) +├── manage.py (29) +├── mkdocs.yml (30) ├── uv.lock -├── pyproject.toml (30) -├── .venv/ (31) -├── .python-version (32) -├── Procfile (33) +├── pyproject.toml (31) +├── .venv/ (32) +├── .python-version (33) +├── Procfile.dev (34) +├── Procfile.pytest (35) └── README.md ``` @@ -122,23 +125,27 @@ sith/ 23. Gestion des statics du site. Override le système de statics de Django. Ajoute l'intégration du scss et du bundler js de manière transparente pour l'utilisateur. -24. Fichier de configuration de coverage. -25. Fichier de configuration de direnv. -26. Contient les variables d'environnement, qui sont susceptibles +24. Module de gestion des services externes. + Offre une API simple pour utiliser les fichiers `Procfile.*`. +25. Fichier de configuration de coverage. +26. Fichier de configuration de direnv. +27. Contient les variables d'environnement, qui sont susceptibles de varier d'une machine à l'autre. -27. Contient des valeurs par défaut pour le `.env` +28. Contient des valeurs par défaut pour le `.env` pouvant convenir à un environnment de développement local -28. Fichier généré automatiquement par Django. C'est lui +29. Fichier généré automatiquement par Django. C'est lui qui permet d'appeler des commandes de gestion du projet avec la syntaxe `python ./manage.py ` -29. Le fichier de configuration de la documentation, +30. Le fichier de configuration de la documentation, avec ses plugins et sa table des matières. -30. Le fichier où sont déclarés les dépendances et la configuration +31. Le fichier où sont déclarés les dépendances et la configuration de certaines d'entre elles. -31. Dossier d'environnement virtuel généré par uv -32. Fichier qui contrôle quelle version de python utiliser pour le projet -33. Fichier qui contrôle les services additionnels à lancer +32. Dossier d'environnement virtuel généré par uv +33. Fichier qui contrôle quelle version de python utiliser pour le projet +34. Fichier qui contrôle les services additionnels à lancer avec le serveur de développement +35. Fichier qui contrôle les services additionnels à lancer + avec pytest ## L'application principale diff --git a/sith/settings.py b/sith/settings.py index 22ccf09e..b283c3e6 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -216,8 +216,7 @@ DATABASES = { "default": env.dj_db_url("DATABASE_URL", conn_max_age=None, conn_health_checks=True) } -if "CACHE_URL" in os.environ: - CACHES = {"default": env.dj_cache_url("CACHE_URL")} +CACHES = {"default": env.dj_cache_url("CACHE_URL")} SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" From 7f8304e407876d2f842b0b5d52ad2b28c46c2a25 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 17 Feb 2025 16:30:25 +0100 Subject: [PATCH 05/11] Add redis to test pipeline --- .github/actions/setup_project/action.yml | 5 +++++ .github/workflows/ci.yml | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/actions/setup_project/action.yml b/.github/actions/setup_project/action.yml index 2d2aae89..bb10a2f5 100644 --- a/.github/actions/setup_project/action.yml +++ b/.github/actions/setup_project/action.yml @@ -9,6 +9,11 @@ runs: packages: gettext version: 1.0 # increment to reset cache + - name: Install Redis + uses: shogo82148/actions-setup-redis@v1 + with: + redis-version: "7.x" + - name: Install uv uses: astral-sh/setup-uv@v5 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa17e14c..554e8055 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: env: SECRET_KEY: notTheRealOne DATABASE_URL: sqlite:///db.sqlite3 + CACHE_URL: redis://127.0.0.1:6379/0 jobs: pre-commit: From aa66fc61ab4e77857a7c909ed728b785783ef901 Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 20 Feb 2025 18:38:15 +0100 Subject: [PATCH 06/11] Apply review comments --- docs/tutorial/install-advanced.md | 22 +++++++++++++++++++++- manage.py | 11 ++++------- pyproject.toml | 4 ++++ {processes => sith}/composer.py | 0 sith/environ.py | 4 ---- sith/pytest.py | 10 ++++------ sith/settings.py | 9 ++++++++- uv.lock | 2 +- 8 files changed, 42 insertions(+), 20 deletions(-) rename {processes => sith}/composer.py (100%) delete mode 100644 sith/environ.py diff --git a/docs/tutorial/install-advanced.md b/docs/tutorial/install-advanced.md index a46cbd6e..897277c1 100644 --- a/docs/tutorial/install-advanced.md +++ b/docs/tutorial/install-advanced.md @@ -77,7 +77,22 @@ uv sync --group prod C'est parce que ces dépendances compilent certains modules à l'installation. -## Configurer Redis +## Désactiver Honcho + +Honcho est utilisé en développement pour simplifier la gestion +des services externes (redis, vite et autres futures). + +En mode production, il est nécessaire de le désactiver puisque normalement +tous ces services sont déjà configurés. + +Pour désactiver Honcho il suffit de sélectionner aucun `PROCFILE_` dans la config. + +```dotenv +PROCFILE_RUNSERVER= +PROCFILE_PYTEST= +``` + +## Configurer Redis en service externe Redis est installé comme dépendance mais pas lancé par défaut. @@ -100,6 +115,11 @@ REDIS_PORT=6379 CACHE_URL=redis://127.0.0.1:${REDIS_PORT}/0 ``` +Si on souhaite configurer redis pour communiquer via un socket : + +```dovenv +CACHE_URL=redis:///path/to/redis-server.sock +``` ## Configurer PostgreSQL diff --git a/manage.py b/manage.py index 71939ec3..444f6799 100755 --- a/manage.py +++ b/manage.py @@ -18,19 +18,16 @@ import sys from django.utils.autoreload import DJANGO_AUTORELOAD_ENV -from processes.composer import start_composer, stop_composer -from sith.environ import env +from sith.composer import start_composer, stop_composer +from sith.settings import PROCFILE_RUNSERVER 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) + if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_RUNSERVER is not None: + start_composer(PROCFILE_RUNSERVER) execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml index d5d3c1f0..7be2162c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,6 +126,10 @@ ignore = [ [tool.ruff.lint.pydocstyle] convention = "google" +[build-system] # A build system is needed to register a pytest plugin +requires = ["hatchling"] +build-backend = "hatchling.build" + [project.entry-points.pytest11] sith = "sith.pytest" diff --git a/processes/composer.py b/sith/composer.py similarity index 100% rename from processes/composer.py rename to sith/composer.py diff --git a/sith/environ.py b/sith/environ.py deleted file mode 100644 index 0b048822..00000000 --- a/sith/environ.py +++ /dev/null @@ -1,4 +0,0 @@ -from environs import Env - -env = Env() -_ = env.read_env() diff --git a/sith/pytest.py b/sith/pytest.py index 3a99a7fc..29250e16 100644 --- a/sith/pytest.py +++ b/sith/pytest.py @@ -1,10 +1,8 @@ import pytest -from processes.composer import start_composer, stop_composer +from .composer import start_composer, stop_composer +from .settings import PROCFILE_PYTEST -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 @@ -15,8 +13,8 @@ from .environ import env @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) + if PROCFILE_PYTEST is not None: + start_composer(PROCFILE_PYTEST) def pytest_unconfigure(config): diff --git a/sith/settings.py b/sith/settings.py index b283c3e6..cc434703 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -42,13 +42,20 @@ 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() +# Composer settings +PROCFILE_RUNSERVER = env.str("PROCFILE_RUNSERVER", None) +PROCFILE_PYTEST = env.str("PROCFILE_PYTEST", None) + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ diff --git a/uv.lock b/uv.lock index 11b00547..f96b078e 100644 --- a/uv.lock +++ b/uv.lock @@ -1513,7 +1513,7 @@ wheels = [ [[package]] name = "sith" version = "3" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "cryptography" }, { name = "dict2xml" }, From e542fe11b9c693312eb0b95cb3374fd5f57dbc89 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 26 Feb 2025 10:42:11 +0100 Subject: [PATCH 07/11] Add a pid file to avoid running honcho multiple times --- manage.py | 7 +++++-- sith/composer.py | 54 ++++++++++++++++++++++++++++++++++++++++++------ sith/pytest.py | 7 +++---- sith/settings.py | 3 +++ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/manage.py b/manage.py index 444f6799..05877790 100755 --- a/manage.py +++ b/manage.py @@ -13,6 +13,8 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +import atexit +import logging import os import sys @@ -22,13 +24,14 @@ from sith.composer import start_composer, stop_composer from sith.settings import PROCFILE_RUNSERVER if __name__ == "__main__": + logging.basicConfig(encoding="utf-8", level=logging.INFO) + 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_RUNSERVER is not None: start_composer(PROCFILE_RUNSERVER) + _ = atexit.register(stop_composer) execute_from_command_line(sys.argv) - - stop_composer() diff --git a/sith/composer.py b/sith/composer.py index 8355e205..e0a97bbd 100644 --- a/sith/composer.py +++ b/sith/composer.py @@ -1,26 +1,68 @@ -import os +import logging import signal import subprocess import sys import psutil -COMPOSER_PID = "COMPOSER_PID" +from sith import settings + + +def get_pid() -> int | None: + """Read the PID file to get the currently running composer if it exists""" + if not settings.COMPOSER_PID_PATH.exists(): + return None + with open(settings.COMPOSER_PID_PATH, "r", encoding="utf8") as f: + return int(f.read()) + + +def write_pid(pid: int): + """Write currently running composer pid in PID file""" + if not settings.COMPOSER_PID_PATH.exists(): + settings.COMPOSER_PID_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(settings.COMPOSER_PID_PATH, "w", encoding="utf8") as f: + _ = f.write(str(pid)) + + +def delete_pid(): + """Delete PID file for cleanup""" + settings.COMPOSER_PID_PATH.unlink(missing_ok=True) + + +def is_composer_running() -> bool: + """Check if the process in the PID file is running""" + pid = get_pid() + if pid is None: + return False + try: + return psutil.Process(pid).is_running() + except psutil.NoSuchProcess: + return False 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 """ + if is_composer_running(): + logging.info(f"Composer is already running with pid {get_pid()}") + logging.info( + f"If this is a mistake, please delete {settings.COMPOSER_PID_PATH} and restart the process" + ) + return process = subprocess.Popen( [sys.executable, "-m", "honcho", "-f", procfile, "start"], ) - os.environ[COMPOSER_PID] = str(process.pid) + write_pid(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)) + if is_composer_running(): + process = psutil.Process(get_pid()) + if process.parent() != psutil.Process(): + logging.info("Currently running composer is controlled by another process") + return process.send_signal(signal.SIGTERM) - process.wait() + _ = process.wait() + delete_pid() diff --git a/sith/pytest.py b/sith/pytest.py index 29250e16..731cd584 100644 --- a/sith/pytest.py +++ b/sith/pytest.py @@ -1,3 +1,5 @@ +import atexit + import pytest from .composer import start_composer, stop_composer @@ -15,7 +17,4 @@ def pytest_load_initial_conftests(early_config, parser, args): """Hook that loads the composer before the pytest-django plugin""" if PROCFILE_PYTEST is not None: start_composer(PROCFILE_PYTEST) - - -def pytest_unconfigure(config): - stop_composer() + _ = atexit.register(stop_composer) diff --git a/sith/settings.py b/sith/settings.py index cc434703..b9acd61d 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -56,6 +56,9 @@ BASE_DIR = Path(__file__).parent.parent.resolve() PROCFILE_RUNSERVER = env.str("PROCFILE_RUNSERVER", None) PROCFILE_PYTEST = env.str("PROCFILE_PYTEST", None) +## File path used to avoid running the composer multiple times at the same time +COMPOSER_PID_PATH = env.path("COMPOSER_PID_PATH", BASE_DIR / "composer.pid") + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ From 728ad157e97d6207fa76dde82047c5b2ba7dcfa3 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 26 Feb 2025 14:37:22 +0100 Subject: [PATCH 08/11] Apply review comments --- .gitignore | 2 +- docs/tutorial/install-advanced.md | 10 +++++++++- sith/composer.py | 5 +++-- sith/settings.py | 15 +++++++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index eb78b41d..65bd0e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,10 @@ sith/search_indexes/ .coverage coverage_report/ node_modules/ +.env # compiled documentation site/ -.env ### Redis ### diff --git a/docs/tutorial/install-advanced.md b/docs/tutorial/install-advanced.md index 897277c1..9fc54a8a 100644 --- a/docs/tutorial/install-advanced.md +++ b/docs/tutorial/install-advanced.md @@ -85,13 +85,21 @@ des services externes (redis, vite et autres futures). En mode production, il est nécessaire de le désactiver puisque normalement tous ces services sont déjà configurés. -Pour désactiver Honcho il suffit de sélectionner aucun `PROCFILE_` dans la config. +Pour désactiver Honcho il suffit de ne sélectionner aucun `PROCFILE_` dans la config. ```dotenv PROCFILE_RUNSERVER= PROCFILE_PYTEST= ``` +!!! note + + Si honcho ne tourne plus, la recompilation automatique + des fichiers statiques ne se fait plus. + Si vous en avez besoin et que vous travaillez sans honcho, + vous devez ouvrir une autre fenêtre de votre terminal + et lancer la commande `npm run serve` + ## Configurer Redis en service externe Redis est installé comme dépendance mais pas lancé par défaut. diff --git a/sith/composer.py b/sith/composer.py index e0a97bbd..b1d90d92 100644 --- a/sith/composer.py +++ b/sith/composer.py @@ -2,6 +2,7 @@ import logging import signal import subprocess import sys +from pathlib import Path import psutil @@ -40,7 +41,7 @@ def is_composer_running() -> bool: return False -def start_composer(procfile: str): +def start_composer(procfile: Path): """Starts the composer and stores the PID as an environment variable This allows for running smoothly with the django reloader """ @@ -51,7 +52,7 @@ def start_composer(procfile: str): ) return process = subprocess.Popen( - [sys.executable, "-m", "honcho", "-f", procfile, "start"], + [sys.executable, "-m", "honcho", "-f", str(procfile), "start"], ) write_pid(process.pid) diff --git a/sith/settings.py b/sith/settings.py index b9acd61d..a95f6725 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -50,11 +50,22 @@ from .honeypot import custom_honeypot_error env = Env() env.read_env() + +@env.parser_for("optional_file") +def optional_file_parser(value: str) -> Path | None: + if not value: + return None + path = Path(value) + if not path.is_file(): + return None + return path + + BASE_DIR = Path(__file__).parent.parent.resolve() # Composer settings -PROCFILE_RUNSERVER = env.str("PROCFILE_RUNSERVER", None) -PROCFILE_PYTEST = env.str("PROCFILE_PYTEST", None) +PROCFILE_RUNSERVER = env.optional_file("PROCFILE_RUNSERVER", None) +PROCFILE_PYTEST = env.optional_file("PROCFILE_PYTEST", None) ## File path used to avoid running the composer multiple times at the same time COMPOSER_PID_PATH = env.path("COMPOSER_PID_PATH", BASE_DIR / "composer.pid") From 75c4c55a32d86b014178b0ed123bf9b0174d3ef5 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 4 Mar 2025 11:59:35 +0100 Subject: [PATCH 09/11] Only run full procfile on runserver --- .env.example | 6 +++--- .gitignore | 1 + Procfile.dev => Procfile.full | 0 Procfile.pytest => Procfile.minimal | 0 docs/tutorial/install-advanced.md | 4 ++-- docs/tutorial/structure.md | 8 ++++---- manage.py | 7 ++++--- sith/pytest.py | 6 +++--- sith/settings.py | 4 ++-- 9 files changed, 19 insertions(+), 17 deletions(-) rename Procfile.dev => Procfile.full (100%) rename Procfile.pytest => Procfile.minimal (100%) diff --git a/.env.example b/.env.example index da4d2ac8..1f050d4a 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,6 @@ 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 +# manage.py, pytest and runserver +PROCFILE_FULL=Procfile.full +PROCFILE_MINIMAL=Procfile.minimal diff --git a/.gitignore b/.gitignore index 65bd0e2e..ecda5902 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ sith/search_indexes/ coverage_report/ node_modules/ .env +*.pid # compiled documentation site/ diff --git a/Procfile.dev b/Procfile.full similarity index 100% rename from Procfile.dev rename to Procfile.full diff --git a/Procfile.pytest b/Procfile.minimal similarity index 100% rename from Procfile.pytest rename to Procfile.minimal diff --git a/docs/tutorial/install-advanced.md b/docs/tutorial/install-advanced.md index 9fc54a8a..b5630d2b 100644 --- a/docs/tutorial/install-advanced.md +++ b/docs/tutorial/install-advanced.md @@ -88,8 +88,8 @@ tous ces services sont déjà configurés. Pour désactiver Honcho il suffit de ne sélectionner aucun `PROCFILE_` dans la config. ```dotenv -PROCFILE_RUNSERVER= -PROCFILE_PYTEST= +PROCFILE_FULL= +PROCFILE_MINIMAL= ``` !!! note diff --git a/docs/tutorial/structure.md b/docs/tutorial/structure.md index 6ab36c6c..6af76354 100644 --- a/docs/tutorial/structure.md +++ b/docs/tutorial/structure.md @@ -82,8 +82,8 @@ sith/ ├── pyproject.toml (31) ├── .venv/ (32) ├── .python-version (33) -├── Procfile.dev (34) -├── Procfile.pytest (35) +├── Procfile.full (34) +├── Procfile.minimal (35) └── README.md ``` @@ -143,9 +143,9 @@ sith/ 32. Dossier d'environnement virtuel généré par uv 33. Fichier qui contrôle quelle version de python utiliser pour le projet 34. Fichier qui contrôle les services additionnels à lancer - avec le serveur de développement + avec le serveur de développement. Serveurs + gestion des statics. 35. Fichier qui contrôle les services additionnels à lancer - avec pytest + avec les autres commandes (pytest, shell…). Serveurs uniquement. ## L'application principale diff --git a/manage.py b/manage.py index 05877790..cb26d55d 100755 --- a/manage.py +++ b/manage.py @@ -21,7 +21,7 @@ import sys from django.utils.autoreload import DJANGO_AUTORELOAD_ENV from sith.composer import start_composer, stop_composer -from sith.settings import PROCFILE_RUNSERVER +from sith.settings import PROCFILE_FULL, PROCFILE_MINIMAL if __name__ == "__main__": logging.basicConfig(encoding="utf-8", level=logging.INFO) @@ -30,8 +30,9 @@ if __name__ == "__main__": from django.core.management import execute_from_command_line - if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_RUNSERVER is not None: - start_composer(PROCFILE_RUNSERVER) + procfile = PROCFILE_FULL if sys.argv[1] == "runserver" else PROCFILE_MINIMAL + if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and procfile is not None: + start_composer(procfile) _ = atexit.register(stop_composer) execute_from_command_line(sys.argv) diff --git a/sith/pytest.py b/sith/pytest.py index 731cd584..844f49d2 100644 --- a/sith/pytest.py +++ b/sith/pytest.py @@ -3,7 +3,7 @@ import atexit import pytest from .composer import start_composer, stop_composer -from .settings import PROCFILE_PYTEST +from .settings import PROCFILE_MINIMAL # it's the first hook loaded by pytest and can only # be defined in a proper pytest plugin @@ -15,6 +15,6 @@ from .settings import PROCFILE_PYTEST @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_PYTEST is not None: - start_composer(PROCFILE_PYTEST) + if PROCFILE_MINIMAL is not None: + start_composer(PROCFILE_MINIMAL) _ = atexit.register(stop_composer) diff --git a/sith/settings.py b/sith/settings.py index a95f6725..dab15139 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -64,8 +64,8 @@ def optional_file_parser(value: str) -> Path | None: BASE_DIR = Path(__file__).parent.parent.resolve() # Composer settings -PROCFILE_RUNSERVER = env.optional_file("PROCFILE_RUNSERVER", None) -PROCFILE_PYTEST = env.optional_file("PROCFILE_PYTEST", None) +PROCFILE_FULL = env.optional_file("PROCFILE_FULL", None) +PROCFILE_MINIMAL = env.optional_file("PROCFILE_MINIMAL", None) ## File path used to avoid running the composer multiple times at the same time COMPOSER_PID_PATH = env.path("COMPOSER_PID_PATH", BASE_DIR / "composer.pid") From 87f790a0441fabf70b09db1b28b22920f30f68a1 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 3 Mar 2025 13:17:53 +0100 Subject: [PATCH 10/11] Don't minify statics in debug mode --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f46df7db..bc6018b1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "compile": "vite build --mode production", "compile-dev": "vite build --mode development", - "serve": "vite build --mode development --watch", + "serve": "vite build --mode development --watch --minify false", "analyse-dev": "vite-bundle-visualizer --mode development", "analyse-prod": "vite-bundle-visualizer --mode production", "check": "biome check --write" From 6b27a97e7b248c6c544ef03b014c02259a20af82 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 4 Mar 2025 14:48:44 +0100 Subject: [PATCH 11/11] Launch multiple honcho files depending on the context --- .env.example | 4 +- Procfile.full | 2 - Procfile.minimal => Procfile.service | 0 Procfile.static | 1 + docs/tutorial/install-advanced.md | 8 +-- docs/tutorial/structure.md | 12 ++--- manage.py | 9 ++-- sith/composer.py | 51 ++++++++++++-------- sith/pytest.py | 8 +-- sith/settings.py | 7 +-- staticfiles/management/commands/runserver.py | 12 +++++ 11 files changed, 66 insertions(+), 48 deletions(-) delete mode 100644 Procfile.full rename Procfile.minimal => Procfile.service (100%) create mode 100644 Procfile.static diff --git a/.env.example b/.env.example index 1f050d4a..2d47ad1f 100644 --- a/.env.example +++ b/.env.example @@ -13,5 +13,5 @@ CACHE_URL=redis://127.0.0.1:${REDIS_PORT}/0 # Used to select which other services to run alongside # manage.py, pytest and runserver -PROCFILE_FULL=Procfile.full -PROCFILE_MINIMAL=Procfile.minimal +PROCFILE_STATIC=Procfile.static +PROCFILE_SERVICE=Procfile.service diff --git a/Procfile.full b/Procfile.full deleted file mode 100644 index 31652ef9..00000000 --- a/Procfile.full +++ /dev/null @@ -1,2 +0,0 @@ -bundler: npm run serve -redis: redis-server --port $REDIS_PORT \ No newline at end of file diff --git a/Procfile.minimal b/Procfile.service similarity index 100% rename from Procfile.minimal rename to Procfile.service diff --git a/Procfile.static b/Procfile.static new file mode 100644 index 00000000..857b2f2d --- /dev/null +++ b/Procfile.static @@ -0,0 +1 @@ +bundler: npm run serve \ No newline at end of file diff --git a/docs/tutorial/install-advanced.md b/docs/tutorial/install-advanced.md index b5630d2b..2da2fc42 100644 --- a/docs/tutorial/install-advanced.md +++ b/docs/tutorial/install-advanced.md @@ -88,15 +88,15 @@ tous ces services sont déjà configurés. Pour désactiver Honcho il suffit de ne sélectionner aucun `PROCFILE_` dans la config. ```dotenv -PROCFILE_FULL= -PROCFILE_MINIMAL= +PROCFILE_STATIC= +PROCFILE_SERVICE= ``` !!! note - Si honcho ne tourne plus, la recompilation automatique + Si `PROCFILE_STATIC` est désactivé, la recompilation automatique des fichiers statiques ne se fait plus. - Si vous en avez besoin et que vous travaillez sans honcho, + Si vous en avez besoin et que vous travaillez sans `PROCFILE_STATIC`, vous devez ouvrir une autre fenêtre de votre terminal et lancer la commande `npm run serve` diff --git a/docs/tutorial/structure.md b/docs/tutorial/structure.md index 6af76354..7c740bde 100644 --- a/docs/tutorial/structure.md +++ b/docs/tutorial/structure.md @@ -82,8 +82,8 @@ sith/ ├── pyproject.toml (31) ├── .venv/ (32) ├── .python-version (33) -├── Procfile.full (34) -├── Procfile.minimal (35) +├── Procfile.static (34) +├── Procfile.service (35) └── README.md ``` @@ -142,10 +142,10 @@ sith/ de certaines d'entre elles. 32. Dossier d'environnement virtuel généré par uv 33. Fichier qui contrôle quelle version de python utiliser pour le projet -34. Fichier qui contrôle les services additionnels à lancer - avec le serveur de développement. Serveurs + gestion des statics. -35. Fichier qui contrôle les services additionnels à lancer - avec les autres commandes (pytest, shell…). Serveurs uniquement. +34. Fichier qui contrôle les commandes à lancer pour gérer la compilation + automatique des static et autres services nécessaires à la command runserver. +35. Fichier qui contrôle les services tiers nécessaires au fonctionnement + du Sith tel que redis. ## L'application principale diff --git a/manage.py b/manage.py index cb26d55d..101696e2 100755 --- a/manage.py +++ b/manage.py @@ -21,7 +21,7 @@ import sys from django.utils.autoreload import DJANGO_AUTORELOAD_ENV from sith.composer import start_composer, stop_composer -from sith.settings import PROCFILE_FULL, PROCFILE_MINIMAL +from sith.settings import PROCFILE_SERVICE if __name__ == "__main__": logging.basicConfig(encoding="utf-8", level=logging.INFO) @@ -30,9 +30,8 @@ if __name__ == "__main__": from django.core.management import execute_from_command_line - procfile = PROCFILE_FULL if sys.argv[1] == "runserver" else PROCFILE_MINIMAL - if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and procfile is not None: - start_composer(procfile) - _ = atexit.register(stop_composer) + if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_SERVICE is not None: + start_composer(PROCFILE_SERVICE) + _ = atexit.register(stop_composer, procfile=PROCFILE_SERVICE) execute_from_command_line(sys.argv) diff --git a/sith/composer.py b/sith/composer.py index b1d90d92..dd3d648a 100644 --- a/sith/composer.py +++ b/sith/composer.py @@ -9,30 +9,37 @@ import psutil from sith import settings -def get_pid() -> int | None: +def get_pid_file(procfile: Path) -> Path: + """Get the PID file associated with a procfile""" + return settings.BASE_DIR / procfile.with_suffix(f"{procfile.suffix}.pid") + + +def get_pid(procfile: Path) -> int | None: """Read the PID file to get the currently running composer if it exists""" - if not settings.COMPOSER_PID_PATH.exists(): + file = get_pid_file(procfile) + if not file.exists(): return None - with open(settings.COMPOSER_PID_PATH, "r", encoding="utf8") as f: + with open(file, "r", encoding="utf8") as f: return int(f.read()) -def write_pid(pid: int): +def write_pid(procfile: Path, pid: int): """Write currently running composer pid in PID file""" - if not settings.COMPOSER_PID_PATH.exists(): - settings.COMPOSER_PID_PATH.parent.mkdir(parents=True, exist_ok=True) - with open(settings.COMPOSER_PID_PATH, "w", encoding="utf8") as f: + file = get_pid_file(procfile) + if not file.exists(): + file.parent.mkdir(parents=True, exist_ok=True) + with open(file, "w", encoding="utf8") as f: _ = f.write(str(pid)) -def delete_pid(): +def delete_pid(procfile: Path): """Delete PID file for cleanup""" - settings.COMPOSER_PID_PATH.unlink(missing_ok=True) + get_pid_file(procfile).unlink(missing_ok=True) -def is_composer_running() -> bool: +def is_composer_running(procfile: Path) -> bool: """Check if the process in the PID file is running""" - pid = get_pid() + pid = get_pid(procfile) if pid is None: return False try: @@ -45,25 +52,29 @@ def start_composer(procfile: Path): """Starts the composer and stores the PID as an environment variable This allows for running smoothly with the django reloader """ - if is_composer_running(): - logging.info(f"Composer is already running with pid {get_pid()}") + if is_composer_running(procfile): logging.info( - f"If this is a mistake, please delete {settings.COMPOSER_PID_PATH} and restart the process" + f"Composer for {procfile} is already running with pid {get_pid(procfile)}" + ) + logging.info( + f"If this is a mistake, please delete {get_pid_file(procfile)} and restart the process" ) return process = subprocess.Popen( [sys.executable, "-m", "honcho", "-f", str(procfile), "start"], ) - write_pid(process.pid) + write_pid(procfile, process.pid) -def stop_composer(): +def stop_composer(procfile: Path): """Stops the composer if it was started before""" - if is_composer_running(): - process = psutil.Process(get_pid()) + if is_composer_running(procfile): + process = psutil.Process(get_pid(procfile)) if process.parent() != psutil.Process(): - logging.info("Currently running composer is controlled by another process") + logging.info( + f"Currently running composer for {procfile} is controlled by another process" + ) return process.send_signal(signal.SIGTERM) _ = process.wait() - delete_pid() + delete_pid(procfile) diff --git a/sith/pytest.py b/sith/pytest.py index 844f49d2..f3cb4ec6 100644 --- a/sith/pytest.py +++ b/sith/pytest.py @@ -3,7 +3,7 @@ import atexit import pytest from .composer import start_composer, stop_composer -from .settings import PROCFILE_MINIMAL +from .settings import PROCFILE_SERVICE # it's the first hook loaded by pytest and can only # be defined in a proper pytest plugin @@ -15,6 +15,6 @@ from .settings import PROCFILE_MINIMAL @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_MINIMAL is not None: - start_composer(PROCFILE_MINIMAL) - _ = atexit.register(stop_composer) + if PROCFILE_SERVICE is not None: + start_composer(PROCFILE_SERVICE) + _ = atexit.register(stop_composer, procfile=PROCFILE_SERVICE) diff --git a/sith/settings.py b/sith/settings.py index dab15139..f75b1ac9 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -64,11 +64,8 @@ def optional_file_parser(value: str) -> Path | None: BASE_DIR = Path(__file__).parent.parent.resolve() # Composer settings -PROCFILE_FULL = env.optional_file("PROCFILE_FULL", None) -PROCFILE_MINIMAL = env.optional_file("PROCFILE_MINIMAL", None) - -## File path used to avoid running the composer multiple times at the same time -COMPOSER_PID_PATH = env.path("COMPOSER_PID_PATH", BASE_DIR / "composer.pid") +PROCFILE_STATIC = env.optional_file("PROCFILE_STATIC", None) +PROCFILE_SERVICE = env.optional_file("PROCFILE_SERVICE", None) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ diff --git a/staticfiles/management/commands/runserver.py b/staticfiles/management/commands/runserver.py index 8dd88538..7138dee1 100644 --- a/staticfiles/management/commands/runserver.py +++ b/staticfiles/management/commands/runserver.py @@ -1,7 +1,13 @@ +import atexit +import os + +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 sith.composer import start_composer, stop_composer from staticfiles.processors import OpenApi @@ -10,4 +16,10 @@ class Command(Runserver): def run(self, **options): OpenApi.compile() + if ( + os.environ.get(DJANGO_AUTORELOAD_ENV) is None + and settings.PROCFILE_STATIC is not None + ): + start_composer(settings.PROCFILE_STATIC) + _ = atexit.register(stop_composer, procfile=settings.PROCFILE_STATIC) super().run(**options)