diff --git a/nixos/modules/services/web-apps/glitchtip.nix b/nixos/modules/services/web-apps/glitchtip.nix index 6a74cda886a1..3660b83807bd 100644 --- a/nixos/modules/services/web-apps/glitchtip.nix +++ b/nixos/modules/services/web-apps/glitchtip.nix @@ -45,19 +45,6 @@ in default = "glitchtip"; }; - listenAddress = lib.mkOption { - type = lib.types.str; - description = "The address to listen on."; - default = "127.0.0.1"; - example = "0.0.0.0"; - }; - - port = lib.mkOption { - type = lib.types.port; - description = "The port to listen on."; - default = 8000; - }; - stateDir = lib.mkOption { type = lib.types.path; description = "State directory of glitchtip."; @@ -66,7 +53,7 @@ in settings = lib.mkOption { description = '' - Configuration of GlitchTip. See for more information. + Configuration of GlitchTip. See for more information and required settings. ''; default = { }; defaultText = lib.literalExpression '' @@ -74,8 +61,14 @@ in DEBUG = 0; DEBUG_TOOLBAR = 0; DATABASE_URL = lib.mkIf config.services.glitchtip.database.createLocally "postgresql://@/glitchtip"; + GLITCHTIP_DOMAIN = lib.mkIf config.services.glitchtip.nginx.createLocally "https://''${config.services.glitchtip.nginx.domain}"; + GLITCHTIP_VERSION = config.services.glitchtip.package.version; + GRANIAN_HOST = "127.0.0.1"; + GRANIAN_PORT = 8000; + GRANIAN_STATIC_PATH_MOUNT = "''${config.services.glitchtip.package}/lib/glitchtip/static"; + GRANIAN_WORKERS = 1; + PYTHONUNBUFFERED = 1; REDIS_URL = lib.mkIf config.services.glitchtip.redis.createLocally "unix://''${config.services.redis.servers.glitchtip.unixSocket}"; - CELERY_BROKER_URL = lib.mkIf config.services.glitchtip.redis.createLocally "redis+socket://''${config.services.redis.servers.glitchtip.unixSocket}"; } ''; example = { @@ -94,9 +87,28 @@ in options = { GLITCHTIP_DOMAIN = lib.mkOption { - type = lib.types.str; + type = lib.types.nullOr lib.types.str; description = "The URL under which GlitchTip is externally reachable."; example = "https://glitchtip.example.com"; + default = null; + }; + + GLITCHTIP_ENABLE_MCP = lib.mkOption { + type = lib.types.bool; + description = "Whether to enable the MCP api."; + default = false; + }; + + GRANIAN_WORKERS = lib.mkOption { + type = lib.types.ints.positive; + description = "Number of granian workers to start"; + default = 1; + }; + + ENABLE_OBSERVABILITY_API = lib.mkOption { + type = lib.types.bool; + description = "Whether to enable the Prometheus metrics endpoint."; + default = false; }; ENABLE_USER_REGISTRATION = lib.mkOption { @@ -132,49 +144,74 @@ in database.createLocally = lib.mkOption { type = lib.types.bool; default = true; - description = '' - Whether to enable and configure a local PostgreSQL database server. - ''; + description = "Whether to enable and configure a local PostgreSQL database server."; + }; + + nginx = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable and configure a local Nginx server."; + }; + + domain = lib.mkOption { + type = lib.types.str; + example = "glitchtip.example.com"; + description = '' + Domain under which GlitchTip will be reachable. + In contrast to `settings.GLITCHTIP_DOMAIN` this option has no protocol. + It will also set `settings.GLITCHTIP_DOMAIN` with the `https://` protocol. + ''; + }; }; redis.createLocally = lib.mkOption { type = lib.types.bool; default = true; - description = '' - Whether to enable and configure a local Redis instance. - ''; - }; - - gunicorn.extraArgs = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "Extra arguments for gunicorn."; - }; - - celery.extraArgs = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "Extra arguments for celery."; + description = "Whether to enable and configure a local Redis instance."; }; }; }; + imports = [ + (lib.mkRenamedOptionModule + [ "services" "glitchtip" "listenAddress" ] + [ "services" "glitchtip" "settings" "GRANIAN_HOST" ] + ) + (lib.mkRenamedOptionModule + [ "services" "glitchtip" "port" ] + [ "services" "glitchtip" "settings" "GRANIAN_PORT" ] + ) + (lib.mkRemovedOptionModule [ "services" "glitchtip" "celery" "extraArgs" ] + "GlitchTip 6 migrated away from celery. Please check the upstream docs how to handle your usecase now." + ) + (lib.mkRemovedOptionModule [ "services" "glitchtip" "gunicorn" "extraArgs" ] + "GlitchTip 6 migrated away from gunicorn. Please check the upstream docs how to handle your usecase now." + ) + ]; + config = lib.mkIf cfg.enable { services.glitchtip.settings = { DEBUG = lib.mkDefault 0; DEBUG_TOOLBAR = lib.mkDefault 0; - PYTHONPATH = "${python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/glitchtip"; - DATABASE_URL = lib.mkIf cfg.database.createLocally "postgresql://@/glitchtip"; - REDIS_URL = lib.mkIf cfg.redis.createLocally "unix://${config.services.redis.servers.glitchtip.unixSocket}"; - CELERY_BROKER_URL = lib.mkIf cfg.redis.createLocally "redis+socket://${config.services.redis.servers.glitchtip.unixSocket}"; GLITCHTIP_VERSION = pkg.version; + GRANIAN_HOST = lib.mkDefault "127.0.0.1"; + GRANIAN_PORT = lib.mkDefault 8000; + GRANIAN_STATIC_PATH_MOUNT = "${pkg}/lib/glitchtip/static"; + GRANIAN_WORKERS = lib.mkDefault 1; + PYTHONPATH = "${python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/glitchtip"; + PYTHONUNBUFFERED = lib.mkDefault 1; + } + // lib.optionalAttrs cfg.database.createLocally { DATABASE_URL = "postgresql://@/glitchtip"; } + // lib.optionalAttrs cfg.nginx.createLocally { GLITCHTIP_DOMAIN = "https://${cfg.nginx.domain}"; } + // lib.optionalAttrs cfg.redis.createLocally { + REDIS_URL = "unix://${config.services.redis.servers.glitchtip.unixSocket}"; }; systemd.services = let commonService = { wantedBy = [ "multi-user.target" ]; - wants = [ "network-online.target" ]; requires = lib.optional cfg.database.createLocally "postgresql.target" @@ -235,35 +272,55 @@ in { glitchtip = commonService // { description = "GlitchTip"; + environment = + environment + // lib.optionalAttrs (cfg.settings.ENABLE_OBSERVABILITY_API && cfg.settings.WORKERS > 1) { + PROMETHEUS_MULTIPROC_DIR = "/tmp/prometheus_multiproc"; + }; + bindsTo = [ "glitchtip-worker.service" ]; + before = [ "glitchtip-worker.service" ]; preStart = '' ${lib.getExe pkg} migrate + ${lib.getExe pkg} createcachetable + ${lib.getExe pkg} maintain_partitions ''; serviceConfig = commonServiceConfig // { ExecStart = '' - ${lib.getExe python.pkgs.gunicorn} \ - --bind=${cfg.listenAddress}:${toString cfg.port} \ - ${lib.concatStringsSep " " cfg.gunicorn.extraArgs} \ - glitchtip.wsgi + ${lib.getExe python.pkgs.granian} \ + --interface ${if cfg.settings.GLITCHTIP_ENABLE_MCP then "asgi" else "asginl"} \ + glitchtip.asgi:application \ + --host ${cfg.settings.GRANIAN_HOST} \ + --port ${toString cfg.settings.GRANIAN_PORT} \ + --workers ${toString cfg.settings.GRANIAN_WORKERS} \ + --no-ws ''; }; }; glitchtip-worker = commonService // { description = "GlitchTip Job Runner"; - + environment = environment // { + IS_WORKER = "1"; + }; serviceConfig = commonServiceConfig // { - ExecStart = '' - ${lib.getExe python.pkgs.celery} \ - -A glitchtip worker \ - -B -s /run/glitchtip/celerybeat-schedule \ - ${lib.concatStringsSep " " cfg.celery.extraArgs} - ''; + ExecStart = "${lib.getExe pkg} runworker --scheduler"; }; }; }; + services.nginx = lib.mkIf cfg.nginx.createLocally { + enable = true; + virtualHosts.${cfg.nginx.domain} = { + forceSSL = lib.mkDefault true; + locations = { + "/".proxyPass = "http://${cfg.settings.GRANIAN_HOST}:${toString cfg.settings.GRANIAN_PORT}"; + "/static/".root = "${pkg}/lib/glitchtip"; + }; + }; + }; + services.postgresql = lib.mkIf cfg.database.createLocally { enable = true; ensureDatabases = [ "glitchtip" ]; @@ -289,15 +346,13 @@ in systemd.tmpfiles.settings.glitchtip."${cfg.stateDir}/uploads".d = { inherit (cfg) user group; }; - environment.systemPackages = - let - glitchtip-manage = pkgs.writeShellScriptBin "glitchtip-manage" '' - set -o allexport - ${lib.toShellVars environment} - ${lib.concatMapStringsSep "\n" (f: "source ${f}") cfg.environmentFiles} - ${config.security.wrapperDir}/sudo -E -u ${cfg.user} ${lib.getExe pkg} "$@" - ''; - in - [ glitchtip-manage ]; + environment.systemPackages = [ + (pkgs.writeShellScriptBin "glitchtip-manage" '' + set -o allexport + ${lib.toShellVars environment} + ${lib.concatMapStringsSep "\n" (f: "source ${f}") cfg.environmentFiles} + ${config.security.wrapperDir}/sudo -E -u ${cfg.user} ${lib.getExe pkg} "$@" + '') + ]; }; } diff --git a/nixos/tests/glitchtip.nix b/nixos/tests/glitchtip.nix index 21b3a0197c48..149873726bef 100644 --- a/nixos/tests/glitchtip.nix +++ b/nixos/tests/glitchtip.nix @@ -1,7 +1,8 @@ { lib, ... }: let - domain = "http://glitchtip.local:8000"; + domain = "glitchtip.local"; + url = "http://${domain}"; in { @@ -16,8 +17,10 @@ in { services.glitchtip = { enable = true; - port = 8000; - settings.GLITCHTIP_DOMAIN = domain; + nginx.createLocally = true; + nginx.domain = domain; + settings.GLITCHTIP_DOMAIN = lib.mkForce url; + settings.CSRF_TRUSTED_ORIGINS = "${url},http://localhost:8000"; environmentFiles = [ (builtins.toFile "glitchtip.env" '' SECRET_KEY=8Hz7YCGzo7fiicHb8Qr22ZqwoIB7lSRx @@ -25,19 +28,23 @@ in ]; }; + services.nginx.virtualHosts.${domain}.forceSSL = false; + environment.systemPackages = [ pkgs.sentry-cli ]; - networking.hosts."127.0.0.1" = [ "glitchtip.local" ]; + networking.hosts."127.0.0.1" = [ domain ]; }; + interactive.sshBackdoor.enable = true; + interactive.defaults.virtualisation.graphics = false; + interactive.nodes.machine = { - services.glitchtip.listenAddress = "0.0.0.0"; - networking.firewall.allowedTCPPorts = [ 8000 ]; + networking.firewall.allowedTCPPorts = [ 80 ]; virtualisation.forwardPorts = [ { from = "host"; host.port = 8000; - guest.port = 8000; + guest.port = 80; } ]; }; @@ -53,7 +60,7 @@ in machine.wait_for_unit("glitchtip-worker.service") machine.wait_for_open_port(8000) - origin_url = "${domain}" + origin_url = "${url}" cookie_jar_path = "/tmp/cookies.txt" curl = f"curl -b {cookie_jar_path} -c {cookie_jar_path} -fS -H 'Origin: {origin_url}'" @@ -89,7 +96,7 @@ in # fetch dsn resp = json.loads(machine.succeed(f"{curl} {origin_url}/api/0/projects/main/test/keys/")) assert len(resp) == 1 - assert re.match(r"^http://[\da-f]+@glitchtip\.local:8000/\d+$", dsn := resp[0]["dsn"]["public"]) + assert re.match(r"^http://[\da-f]+@glitchtip\.local/\d+$", dsn := resp[0]["dsn"]["public"]) # send event machine.succeed(f"SENTRY_DSN={dsn} sentry-cli send-event -m 'hello world'") diff --git a/pkgs/by-name/gl/glitchtip/frontend.nix b/pkgs/by-name/gl/glitchtip/frontend.nix index 07f14a72c636..23e6504dd076 100644 --- a/pkgs/by-name/gl/glitchtip/frontend.nix +++ b/pkgs/by-name/gl/glitchtip/frontend.nix @@ -5,22 +5,27 @@ fetchNpmDeps, jq, moreutils, + nodejs_22, }: buildNpmPackage (finalAttrs: { pname = "glitchtip-frontend"; - version = "5.2.1"; + version = "6.1.5"; src = fetchFromGitLab { owner = "glitchtip"; repo = "glitchtip-frontend"; tag = "v${finalAttrs.version}"; - hash = "sha256-aqGgaVjJogG3mDooQVpR59SR0HDuMPfKuB1i0Z/AMs8="; + hash = "sha256-YngWs12wJLi3NgRT3RhGYy38dhOqBATYPMsXEbeoycU="; }; + nodejs = nodejs_22; + npmDeps = fetchNpmDeps { + name = "${finalAttrs.pname}-${finalAttrs.version}-npm-deps"; inherit (finalAttrs) src; - hash = "sha256-urho5XwUJL7m8/xxv9EvH0MxQIW5TG7nOBSIa77RhJc="; + npmDepsFetcherVersion = 3; + hash = "sha256-8T2Ci8S0YHyzY1jjgcNXt5mxUy/4toJrD2edUxtJz3M="; }; postPatch = '' diff --git a/pkgs/by-name/gl/glitchtip/package.nix b/pkgs/by-name/gl/glitchtip/package.nix index 1c420539e06b..0f02c9d83c0f 100644 --- a/pkgs/by-name/gl/glitchtip/package.nix +++ b/pkgs/by-name/gl/glitchtip/package.nix @@ -1,8 +1,7 @@ { lib, - python313, + python314, fetchFromGitLab, - fetchPypi, callPackage, stdenv, makeWrapper, @@ -10,22 +9,10 @@ }: let - python = python313.override { + python = python314.override { self = python; packageOverrides = final: prev: { - django = final.django_5; - django-csp = prev.django-csp.overridePythonAttrs rec { - version = "4.0"; - src = fetchPypi { - inherit version; - pname = "django_csp"; - hash = "sha256-snAQu3Ausgo9rTKReN8rYaK4LTOLcPvcE8OjvShxKDM="; - }; - }; - django-ninja-cursor-pagination = prev.django-ninja-cursor-pagination.overridePythonAttrs { - # checks are failing with django 5 - doCheck = false; - }; + django = final.django_6; }; }; @@ -34,16 +21,15 @@ let [ aiohttp anonymizeip + arro3-core + arro3-io boto3 brotli - celery - celery-batches cxxfilt django django-allauth django-anymail django-cors-headers - django-csp django-environ django-extensions django-import-export @@ -54,32 +40,33 @@ let django-postgres-partition django-prometheus django-storages - django-valkey + django-vcache + django-vtasks + duckdb google-cloud-logging granian - gunicorn + mcp + minidump orjson psycopg pydantic sentry-sdk symbolic user-agents - uvicorn + uuid6 uwsgi-chunked whitenoise ] - ++ celery.optional-dependencies.redis - ++ celery.optional-dependencies.sqlalchemy ++ django-allauth.optional-dependencies.headless-spec ++ django-allauth.optional-dependencies.mfa ++ django-allauth.optional-dependencies.socialaccount ++ django-storages.optional-dependencies.boto3 ++ django-storages.optional-dependencies.azure ++ django-storages.optional-dependencies.google - ++ django-valkey.optional-dependencies.libvalkey - ++ django-valkey.optional-dependencies.lz4 + ++ django-vtasks.optional-dependencies.valkey ++ granian.optional-dependencies.reload ++ granian.optional-dependencies.uvloop + ++ mcp.optional-dependencies.cli ++ psycopg.optional-dependencies.c ++ psycopg.optional-dependencies.pool ++ pydantic.optional-dependencies.email; @@ -89,16 +76,24 @@ in stdenv.mkDerivation (finalAttrs: { pname = "glitchtip"; - version = "5.2.1"; + version = "6.1.4"; pyproject = true; src = fetchFromGitLab { owner = "glitchtip"; repo = "glitchtip-backend"; tag = "v${finalAttrs.version}"; - hash = "sha256-FxkIbSrIyvLnD9n/Cb2udOhsnoC/bwGAfCRB1L/fhwo="; + hash = "sha256-wju/QbIwdtNYQmRppCfjoaqb++spFZbqAsvBwwZyeUM="; }; + postPatch = '' + echo 'import os + ALLAUTH_TRUSTED_CLIENT_IP_HEADER = os.getenv( + "ALLAUTH_TRUSTED_CLIENT_IP_HEADER", + None + )' >> glitchtip/settings.py + ''; + propagatedBuildInputs = pythonPackages; nativeBuildInputs = [ diff --git a/pkgs/development/python-modules/django-autoslug/default.nix b/pkgs/development/python-modules/django-autoslug/default.nix index c9749a1c90da..119851f3b34a 100644 --- a/pkgs/development/python-modules/django-autoslug/default.nix +++ b/pkgs/development/python-modules/django-autoslug/default.nix @@ -3,7 +3,6 @@ buildPythonPackage, fetchFromGitHub, setuptools, - wheel, django, django-modeltranslation, }: @@ -20,9 +19,15 @@ buildPythonPackage rec { hash = "sha256-IRLY4VaKYXVkSgU/zdY+PSmGrcFB2FlE5L7j0FqisRM="; }; + postPatch = lib.optionalString (lib.versionAtLeast django.version "6.0") '' + # sadly upstream does not use pytest, so we must patch the one failing test or we could not run any tests at all + # see https://github.com/justinmayer/django-autoslug/issues/87 + substituteInPlace autoslug/tests/tests.py \ + --replace-fail "assert b.slug == 'hello_world-2'" "assert b.slug == 'hello_world_2'" + ''; + build-system = [ setuptools - wheel ]; dependencies = [ django ]; diff --git a/pkgs/development/python-modules/django-extensions/default.nix b/pkgs/development/python-modules/django-extensions/default.nix index b2295131331e..257f500afd4f 100644 --- a/pkgs/development/python-modules/django-extensions/default.nix +++ b/pkgs/development/python-modules/django-extensions/default.nix @@ -36,6 +36,12 @@ buildPythonPackage rec { hash = "sha256-WgO/bDe4anQCc1q2Gdq3W70yDqDgmsvn39Qf9ZNVXuE="; }; + patches = lib.optionals (lib.versionAtLeast django.version "6.0") [ + # Fix some tests when run with Django 6 + # see https://github.com/django-extensions/django-extensions/pull/1979 + ./django_6-compat.diff + ]; + build-system = [ setuptools ]; dependencies = [ diff --git a/pkgs/development/python-modules/django-extensions/django_6-compat.diff b/pkgs/development/python-modules/django-extensions/django_6-compat.diff new file mode 100644 index 000000000000..6996a24a8a46 --- /dev/null +++ b/pkgs/development/python-modules/django-extensions/django_6-compat.diff @@ -0,0 +1,111 @@ +From 5c096ad7a59a3e9edee66e27fbae46c30e4abd6b Mon Sep 17 00:00:00 2001 +From: asherzod1 +Date: Wed, 11 Mar 2026 00:53:19 +0500 +Subject: [PATCH] fix: add Django 6 compatibility for signals, constraints, and + random char field + +--- + django_extensions/db/fields/__init__.py | 18 +++++++++++++----- + .../management/commands/list_signals.py | 4 +++- + tests/test_management_command.py | 6 +++++- + tests/testapp/models.py | 7 ++++++- + 4 files changed, 27 insertions(+), 8 deletions(-) + +diff --git a/django_extensions/db/fields/__init__.py b/django_extensions/db/fields/__init__.py +index 938254e1f..c233cee84 100644 +--- a/django_extensions/db/fields/__init__.py ++++ b/django_extensions/db/fields/__init__.py +@@ -86,7 +86,11 @@ def find_unique(self, model_instance, field, iterator, *args): + + new = next(iterator) + kwargs[self.attname] = new +- while not new or queryset.filter(query, **kwargs): ++ while True: ++ matching = queryset.filter(query, **kwargs) ++ has_match = matching.exists() if hasattr(matching, "exists") else bool(matching) ++ if new and not has_match: ++ break + new = next(iterator) + kwargs[self.attname] = new + setattr(model_instance, self.attname, new) +@@ -388,10 +392,14 @@ def in_unique_together(self, model_instance): + return False + + def pre_save(self, model_instance, add): +- if (not add or self.keep_default) and getattr( +- model_instance, self.attname +- ) != "": +- return getattr(model_instance, self.attname) ++ current_value = getattr(model_instance, self.attname) ++ # Django 6 may call pre_save multiple times for inserts; if we've already ++ # populated the field value, reuse it instead of regenerating. ++ if current_value not in ("", None): ++ return current_value ++ ++ if (not add or self.keep_default) and current_value != "": ++ return current_value + + population = "" + if self.include_alpha: +diff --git a/django_extensions/management/commands/list_signals.py b/django_extensions/management/commands/list_signals.py +index 9cadc4f85..e7bc9d7f6 100644 +--- a/django_extensions/management/commands/list_signals.py ++++ b/django_extensions/management/commands/list_signals.py +@@ -52,7 +52,9 @@ def handle(self, *args, **options): + for signal in signals: + signal_name = SIGNAL_NAMES.get(signal, "unknown") + for receiver in signal.receivers: +- if django.VERSION >= (5, 0): ++ if django.VERSION >= (6, 0): ++ lookup, receiver, _sender, is_async = receiver ++ elif django.VERSION >= (5, 0): + lookup, receiver, is_async = receiver + else: + lookup, receiver = receiver +diff --git a/tests/test_management_command.py b/tests/test_management_command.py +index ea205ed72..0fb9dc27b 100644 +--- a/tests/test_management_command.py ++++ b/tests/test_management_command.py +@@ -3,6 +3,7 @@ + import logging + import importlib + ++import django + from django.core.management import ( + call_command, + find_commands, +@@ -421,7 +422,10 @@ def test_field_class(self): + stdout=out, + ) + self.output = out.getvalue() +- self.assertIn("id - AutoField", self.output) ++ if django.VERSION >= (6, 0): ++ self.assertIn("id - BigAutoField", self.output) ++ else: ++ self.assertIn("id - AutoField", self.output) + self.assertIn("char_field - CharField", self.output) + self.assertIn("integer_field - IntegerField", self.output) + self.assertIn("foreign_key_field - ForeignKey", self.output) +diff --git a/tests/testapp/models.py b/tests/testapp/models.py +index 278402f51..3f31efcda 100644 +--- a/tests/testapp/models.py ++++ b/tests/testapp/models.py +@@ -1,4 +1,5 @@ + # -*- coding: utf-8 -*- ++import django + from django.db import models + from django.contrib.auth import get_user_model + from django.db.models import UniqueConstraint +@@ -178,7 +179,11 @@ class Meta: + fields=("common_field", "uniq_field"), name="unique_common_uniq_pair" + ), + models.CheckConstraint( +- check=~models.Q(common_field=models.F("another_common_field")), ++ **( ++ {"condition": ~models.Q(common_field=models.F("another_common_field"))} ++ if django.VERSION >= (5, 2) ++ else {"check": ~models.Q(common_field=models.F("another_common_field"))} ++ ), + name="common_and_another_common_differ", + ), + ] diff --git a/pkgs/development/python-modules/django-guardian/default.nix b/pkgs/development/python-modules/django-guardian/default.nix index 820bd1077652..b6e00c831edf 100644 --- a/pkgs/development/python-modules/django-guardian/default.nix +++ b/pkgs/development/python-modules/django-guardian/default.nix @@ -6,6 +6,7 @@ django, pytestCheckHook, pytest-django, + pytest-xdist, setuptools, }: @@ -29,6 +30,7 @@ buildPythonPackage rec { django-environ pytestCheckHook pytest-django + pytest-xdist ]; pythonImportsCheck = [ "guardian" ]; diff --git a/pkgs/development/python-modules/django-prometheus/default.nix b/pkgs/development/python-modules/django-prometheus/default.nix index ff97896101d6..b2526d41c4ad 100644 --- a/pkgs/development/python-modules/django-prometheus/default.nix +++ b/pkgs/development/python-modules/django-prometheus/default.nix @@ -28,6 +28,10 @@ buildPythonPackage rec { --replace-fail '"pytest-runner"' "" ''; + pythonRelaxDeps = [ + "django" + ]; + build-system = [ setuptools ]; dependencies = [ prometheus-client ]; diff --git a/pkgs/development/python-modules/django-vcache/default.nix b/pkgs/development/python-modules/django-vcache/default.nix new file mode 100644 index 000000000000..a2cad50a356f --- /dev/null +++ b/pkgs/development/python-modules/django-vcache/default.nix @@ -0,0 +1,60 @@ +{ + lib, + buildPythonPackage, + croniter, + django, + fetchFromGitLab, + hatchling, + ormsgpack, + prometheus-client, + pythonOlder, + valkey, + zstd, +}: + +buildPythonPackage rec { + pname = "django-vcache"; + version = "1.0.0"; + pyproject = true; + + src = fetchFromGitLab { + owner = "glitchtip"; + repo = "django-vcache"; + tag = "v${version}"; + hash = "sha256-bOHEw4nl82tFjHiJdmyW0LleKMpjUh8uu4crGp6IsWY="; + }; + + build-system = [ hatchling ]; + + dependencies = [ + django + ormsgpack + croniter + valkey + ] + ++ valkey.optional-dependencies.libvalkey + ++ lib.optional (pythonOlder "3.14") zstd; + + optional-dependencies = { + metrics = [ prometheus-client ]; + valkey = [ valkey ] ++ valkey.optional-dependencies.libvalkey; + }; + + pythonImportsCheck = [ "django_vcache" ]; + + # requires valkey sentinel cluster + doCheck = false; + + meta = { + description = "Specialized, lightweight Django cache backend for Valkey"; + homepage = "https://gitlab.com/glitchtip/django-vcache/"; + changelog = "https://gitlab.com/glitchtip/django-vcache/-/blob/main/CHANGELOG.md#${ + lib.replaceString "." "" version + }"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ + defelo + felbinger + ]; + }; +} diff --git a/pkgs/development/python-modules/django-vtasks/default.nix b/pkgs/development/python-modules/django-vtasks/default.nix new file mode 100644 index 000000000000..ca0c1ec7e30e --- /dev/null +++ b/pkgs/development/python-modules/django-vtasks/default.nix @@ -0,0 +1,90 @@ +{ + lib, + buildPythonPackage, + croniter, + dj-database-url, + django-valkey, + django, + fetchFromGitLab, + hatchling, + orjson, + postgresql, + postgresqlTestHook, + prometheus-client, + psycopg, + pytest-asyncio, + pytest-django, + pytestCheckHook, + pythonOlder, + redisTestHook, + valkey, + zstandard, +}: + +buildPythonPackage rec { + pname = "django-vtasks"; + version = "1.0.3"; + pyproject = true; + + src = fetchFromGitLab { + owner = "glitchtip"; + repo = "django-vtasks"; + tag = "v${version}"; + hash = "sha256-75W63HsLBT4EPQCiAXjd9qr6n07/2e5GCUNWeDzXUq0="; + }; + + postPatch = '' + # upstream does not use pytest to run the tests, but we just need to patch one async test to do so + substituteInPlace scripts/test_stale_conn.py \ + --replace-fail "import asyncio" "import asyncio; import pytest" \ + --replace-fail "async def test_async_stale_connection():" "@pytest.mark.asyncio + async def test_async_stale_connection():" + ''; + + build-system = [ hatchling ]; + + dependencies = [ + django + orjson + croniter + ] + ++ lib.optional (pythonOlder "3.14") zstandard; + + optional-dependencies = { + metrics = [ prometheus-client ]; + valkey = [ valkey ] ++ valkey.optional-dependencies.libvalkey; + }; + + pythonImportsCheck = [ "django_vtasks" ]; + + env = { + DATABASE_URL = "postgresql://postgres@%2Fbuild%2Frun%2Fpostgresql"; + VALKEY_URL = "redis://127.0.0.1:6379"; + }; + + nativeCheckInputs = [ + django # must come first as vtasks only works with django 6 + + dj-database-url + django-valkey + postgresql + postgresqlTestHook + psycopg + pytest-asyncio + pytest-django + pytestCheckHook + redisTestHook # contains valkey + ]; + + meta = { + description = "Very fast valkey/postgres django tasks backend"; + homepage = "https://gitlab.com/glitchtip/django-vtasks"; + changelog = "https://gitlab.com/glitchtip/django-vtasks/-/releases/${src.tag}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ + defelo + felbinger + ]; + broken = lib.versionOlder (lib.versions.major django.version) "6"; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index af1b0d99a1cb..a49a8833b40a 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -4412,12 +4412,16 @@ self: super: with self; { django-valkey = callPackage ../development/python-modules/django-valkey { }; + django-vcache = callPackage ../development/python-modules/django-vcache { }; + django-versatileimagefield = callPackage ../development/python-modules/django-versatileimagefield { }; django-vite = callPackage ../development/python-modules/django-vite { }; + django-vtasks = callPackage ../development/python-modules/django-vtasks { }; + django-waffle = callPackage ../development/python-modules/django-waffle { }; django-weasyprint = callPackage ../development/python-modules/django-weasyprint { };