diff --git a/.github/workflows/behave.yml b/.github/workflows/behave.yml
index b72b0919..2cdb52b7 100644
--- a/.github/workflows/behave.yml
+++ b/.github/workflows/behave.yml
@@ -80,7 +80,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: coverage-report
- path: coverage.xml
+ path: django/coverage.xml
- name: Display Coverage Metrics
if: matrix.python-version == '3.12'
@@ -88,6 +88,7 @@ jobs:
with:
minimum_coverage: "50"
report_name: "Django Pytest/Behave Coverage"
+ path: django/coverage.xml
- name: Check Docker state (post-test)
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 7139262f..7148d8d2 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -61,25 +61,26 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install uv pytest-github-actions-annotate-failures
- uv pip install --system -r requirements/local.txt --prerelease=allow
+ uv pip install --system -r django/requirements/local.txt --prerelease=allow
- name: Apply migrations
env:
DATABASE_URL: "postgres://scram:@localhost:5432/test_scram_${{ matrix.python-version }}"
run: |
+ cd django/src
python manage.py makemigrations --noinput
python manage.py migrate --noinput
- name: Check for missing migrations
env:
DATABASE_URL: "postgres://scram:@localhost:5432/test_scram_${{ matrix.python-version }}"
- run: python manage.py makemigrations --check
+ run: cd django/src && python manage.py makemigrations --check
- name: Run Pytest
env:
DATABASE_URL: "postgres://scram:@localhost:5432/test_scram_${{ matrix.python-version }}"
REDIS_HOST: "localhost"
- run: pytest
+ run: cd django/src && pytest
- name: Install Scheduler Dependencies
run: |
diff --git a/.github/workflows/pytest_next_python.yml b/.github/workflows/pytest_next_python.yml
index cb3e041f..59c7865b 100644
--- a/.github/workflows/pytest_next_python.yml
+++ b/.github/workflows/pytest_next_python.yml
@@ -56,7 +56,7 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install uv
- uv pip install --system -r requirements/local.txt --prerelease=allow
+ uv pip install --system -r django/requirements/local.txt --prerelease=allow
# https://github.com/pytest-dev/pytest-github-actions-annotate-failures/pull/68 isn't yet in a release
uv pip install --system git+https://github.com/pytest-dev/pytest-github-actions-annotate-failures.git@6e66cd895fe05cd09be8bad58f5d79110a20385f
@@ -64,11 +64,13 @@ jobs:
env:
DATABASE_URL: "postgres://scram:@localhost:5432/test_scram"
run: |
+ cd django/src
python manage.py makemigrations --noinput || true
python manage.py migrate --fake-initial --noinput -v 2
- name: Check for duplicate migrations
run: |
+ cd django/src
if python manage.py makemigrations --dry-run | grep "No changes detected"; then
echo "No duplicate migrations detected."
else
@@ -80,4 +82,4 @@ jobs:
DATABASE_URL: "postgres://scram:@localhost:5432/test_scram"
REDIS_HOST: "localhost"
run: |
- pytest || echo "::warning:: Failed on future Python version ${{ matrix.python-version }}."
+ cd django/src && pytest || echo "::warning:: Failed on future Python version ${{ matrix.python-version }}."
diff --git a/.idea/runConfigurations/Django_Debugger.xml b/.idea/runConfigurations/Django_Debugger.xml
index 83810494..850059c3 100644
--- a/.idea/runConfigurations/Django_Debugger.xml
+++ b/.idea/runConfigurations/Django_Debugger.xml
@@ -6,7 +6,7 @@
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 279e24a2..e5572b1c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,13 +4,13 @@ fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.14.14
+ rev: v0.15.12
hooks:
- id: ruff-format
- id: ruff-check
@@ -26,7 +26,7 @@ repos:
always_run: true
- repo: https://github.com/astral-sh/uv-pre-commit
- rev: 0.10.7
+ rev: 0.11.8
hooks:
- id: uv-lock
args: [--check]
diff --git a/.vscode/launch.json b/.vscode/launch.json
index ccacdab1..d9abe396 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,7 +14,7 @@
},
"pathMappings": [
{
- "localRoot": "${workspaceFolder}",
+ "localRoot": "${workspaceFolder}/django",
"remoteRoot": "/app"
}
]
@@ -29,7 +29,7 @@
},
"pathMappings": [
{
- "localRoot": "${workspaceFolder}",
+ "localRoot": "${workspaceFolder}/django",
"remoteRoot": "/app"
}
]
diff --git a/Makefile b/Makefile
index 061d7cac..75501f61 100644
--- a/Makefile
+++ b/Makefile
@@ -26,17 +26,17 @@ compose.override.yml:
## behave-all: runs behave inside the containers against all of your features
.Phony: behave-all
behave-all: compose.override.yml
- @docker compose run --rm django coverage run -a manage.py behave --no-input --simple
+ @docker compose run --rm -w /app -e PYTHONPATH=/app/src django coverage run -a src/manage.py behave --no-input --simple
## behave: runs behave inside the containers against a specific feature (append FEATURE=feature_name_here)
.Phony: behave
behave: compose.override.yml
- @docker compose run --rm django python manage.py behave --no-input --simple -i $(FEATURE)
+ @docker compose run --rm -w /app -e PYTHONPATH=/app/src django python src/manage.py behave --no-input --simple -i $(FEATURE)
## integration-tests: runs multi-instance system tests against docker compose running containers
.Phony: integration-tests
integration-tests: run
- @docker compose exec -T django coverage run -a manage.py behave --no-input --use-existing-database scram/route_manager/tests/integration
+ @docker compose exec -T -w /app -e PYTHONPATH=/app/src django coverage run -a src/manage.py behave --no-input --use-existing-database src/scram/route_manager/tests/integration
## behave-translator
.Phony: behave-translator
@@ -52,8 +52,8 @@ build: compose.override.yml
## coverage.xml: generate coverage from test runs
coverage.xml: pytest behave-all integration-tests behave-translator
- @docker compose run --rm django coverage report
- @docker compose run --rm django coverage xml
+ @docker compose run --rm -w /app django coverage report
+ @docker compose run --rm -w /app django coverage xml
## ci-test: runs all tests just like Github CI does
.Phony: ci-test
@@ -127,7 +127,7 @@ pass-reset: compose.override.yml
## pytest: runs pytest inside the containers
.Phony: pytest
pytest: compose.override.yml
- @docker compose run --rm django coverage run -m pytest
+ @docker compose run --rm -w /app -e PYTHONPATH=/app/src django coverage run -m pytest
## pytest-scheduler: runs scheduler package tests with coverage
.Phony: pytest-scheduler
@@ -157,7 +157,7 @@ tail-log: compose.override.yml
## type-check: static type checking
.Phony: type-check
type-check: compose.override.yml
- @docker compose run --rm django mypy scram
+ @docker compose run --rm -w /app -e PYTHONPATH=/app/src django mypy src/scram
## docs-build: build the documentation
.Phony: docs-build
diff --git a/compose.override.local.yml b/compose.override.local.yml
index 44d7c4b6..2d6c754b 100644
--- a/compose.override.local.yml
+++ b/compose.override.local.yml
@@ -10,7 +10,7 @@ services:
dockerfile: ./compose/local/django/Dockerfile
image: scram_local_django
volumes:
- - $CI_PROJECT_DIR:/app:z
+ - $CI_PROJECT_DIR/django:/app:z
- /tmp/profile_data:/tmp/profile_data
env_file:
- ./.envs/.local/.django
@@ -31,7 +31,7 @@ services:
dockerfile: ./compose/local/django/Dockerfile
image: scram_local_django
volumes:
- - $CI_PROJECT_DIR:/app:z
+ - $CI_PROJECT_DIR/django:/app:z
- /tmp/profile_data:/tmp/profile_data
env_file:
- ./.envs/.local/.django
diff --git a/compose.override.production.yml b/compose.override.production.yml
index 46e076b7..463320db 100644
--- a/compose.override.production.yml
+++ b/compose.override.production.yml
@@ -21,7 +21,7 @@ services:
- path: '/etc/vault.d/secrets/kv_root_security.env'
required: false
volumes:
- - ./staticfiles:/staticfiles
+ - ./django/staticfiles:/staticfiles
healthcheck:
test: ["CMD", "curl", "-f", "http://django:5000/process_updates/"]
@@ -58,7 +58,7 @@ services:
- ./compose/production/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem:/etc/ssl/server.crt
- /etc/letsencrypt/live/${HOSTNAME}/privkey.pem:/etc/ssl/server.key
- - ./staticfiles:/staticfiles
+ - ./django/staticfiles:/staticfiles
ports:
- "443:443"
- "80:80"
diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile
index b2ba11c5..bf626e50 100644
--- a/compose/local/django/Dockerfile
+++ b/compose/local/django/Dockerfile
@@ -18,7 +18,7 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
# Requirements are installed here to ensure they will be cached.
-COPY ./requirements /requirements
+COPY ./django/requirements /requirements
RUN pip install uv
RUN uv pip install --system -r /requirements/local.txt --prerelease=allow
@@ -30,7 +30,7 @@ COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
-ENV PATH="$PATH:/app"
-WORKDIR /app
+ENV PATH="$PATH:/app/src"
+WORKDIR /app/src
ENTRYPOINT ["/entrypoint"]
diff --git a/compose/local/docs/Dockerfile b/compose/local/docs/Dockerfile
index a7ac21e6..b4890a71 100644
--- a/compose/local/docs/Dockerfile
+++ b/compose/local/docs/Dockerfile
@@ -24,7 +24,7 @@ RUN apt-get update \
# Only re-run the pip install if these files have changed
-COPY requirements/base.txt requirements/local.txt requirements/production.txt /app/requirements/
+COPY django/requirements/base.txt django/requirements/local.txt django/requirements/production.txt /app/requirements/
RUN pip install uv
RUN uv pip install --system -r /app/requirements/local.txt -r /app/requirements/production.txt --prerelease=allow
diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile
index a83f74d1..5d05ddc9 100644
--- a/compose/production/django/Dockerfile
+++ b/compose/production/django/Dockerfile
@@ -21,7 +21,7 @@ RUN addgroup --system django \
&& adduser --system --ingroup django django
# Requirements are installed here to ensure they will be cached.
-COPY ./requirements/ /requirements
+COPY ./django/requirements/ /requirements
RUN pip install uv
RUN uv pip install --system --no-cache-dir -r /requirements/production.txt \
&& rm -rf /requirements
@@ -34,10 +34,10 @@ COPY --chown=django:django ./compose/production/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
-COPY --chown=django:django . /app
+COPY --chown=django:django ./django /app
USER django
-WORKDIR /app
+WORKDIR /app/src
ENTRYPOINT ["/entrypoint"]
diff --git a/compose/production/django/start b/compose/production/django/start
index ba07ddd4..5d4fa09c 100644
--- a/compose/production/django/start
+++ b/compose/production/django/start
@@ -5,7 +5,7 @@ set -o pipefail
set -o nounset
-python /app/manage.py collectstatic --noinput
+python /app/src/manage.py collectstatic --noinput
-/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker
+/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app/src -k uvicorn.workers.UvicornWorker
diff --git a/config/routing.py b/config/routing.py
deleted file mode 100644
index 10a63105..00000000
--- a/config/routing.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""Define URLs for the WebSocket consumers."""
-
-from django.urls import re_path
-
-from . import consumers
-
-websocket_urlpatterns = [
- re_path(r"ws/route_manager/translator_(?P\w+)/$", consumers.TranslatorConsumer.as_asgi()),
- re_path(r"ws/route_manager/webui_(?P\w+)/$", consumers.WebUIConsumer.as_asgi()),
-]
diff --git a/locale/README.rst b/django/locale/README.rst
similarity index 100%
rename from locale/README.rst
rename to django/locale/README.rst
diff --git a/django/pyproject.toml b/django/pyproject.toml
new file mode 100644
index 00000000..3556c13d
--- /dev/null
+++ b/django/pyproject.toml
@@ -0,0 +1,83 @@
+[project]
+name = "scram-django"
+version = "1.5.1"
+
+# ==== pytest ====
+[tool.pytest.ini_options]
+addopts = [
+ "--ds=config.settings.test",
+ "--reuse-db",
+]
+minversion = "6.0"
+pythonpath = ["src"]
+python_files = [
+ "tests.py",
+ "test_*.py",
+]
+
+# ==== Coverage ====
+[tool.coverage.run]
+branch = true
+data_file = "coverage.coverage"
+include = ["src/scram/*", "src/config/*"]
+omit = ["**/migrations/*", "src/scram/contrib/*", "*/tests/*"]
+plugins = ["django_coverage_plugin"]
+
+[tool.coverage.report]
+exclude_also = [
+ "if debug:",
+ "if self.debug:",
+ "if settings.DEBUG",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "if __name__ == .__main__.:",
+]
+
+# ===== ruff ====
+# Base ruff config is inherited from the root pyproject.toml.
+[tool.ruff]
+exclude = ["migrations"]
+
+[tool.ruff.lint.per-file-ignores]
+"src/scram/route_manager/**" = [
+ "DOC201", # documenting return values
+]
+"src/scram/users/**" = [
+ "DOC201", # documenting return values
+ "FBT001", # minimal issue; don't need to mess with in the User app
+ "PLR2004", # magic values when checking HTTP status codes
+]
+"test.py" = [
+ "S105", # hardcoded password as argument
+]
+
+# ==== mypy ====
+[tool.mypy]
+check_untyped_defs = true
+exclude = ["src/scram/route_manager/tests"]
+ignore_missing_imports = true
+plugins = [
+ "mypy_django_plugin.main",
+ # We did used to have mypy_drf_plugin.main but it seems like it never actually worked...
+]
+python_version = "3.12"
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unused_ignores = true
+
+[[tool.mypy.overrides]]
+# Django migrations should not produce any errors:
+ignore_errors = true
+module = "*.migrations.*"
+
+[tool.django-stubs]
+django_settings_module = "config.settings.test"
+
+# ==== behave ====
+[tool.behave]
+paths = ["src/scram/route_manager/tests/acceptance"]
+stderr_capture = false
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/requirements/base.txt b/django/requirements/base.txt
similarity index 100%
rename from requirements/base.txt
rename to django/requirements/base.txt
diff --git a/requirements/local.txt b/django/requirements/local.txt
similarity index 100%
rename from requirements/local.txt
rename to django/requirements/local.txt
diff --git a/requirements/production.txt b/django/requirements/production.txt
similarity index 100%
rename from requirements/production.txt
rename to django/requirements/production.txt
diff --git a/config/__init__.py b/django/src/config/__init__.py
similarity index 100%
rename from config/__init__.py
rename to django/src/config/__init__.py
diff --git a/config/api_router.py b/django/src/config/api_router.py
similarity index 100%
rename from config/api_router.py
rename to django/src/config/api_router.py
diff --git a/config/asgi.py b/django/src/config/asgi.py
similarity index 81%
rename from config/asgi.py
rename to django/src/config/asgi.py
index 5e11d1fa..a93dcdbe 100644
--- a/config/asgi.py
+++ b/django/src/config/asgi.py
@@ -9,8 +9,6 @@
import logging
import os
-import sys
-from pathlib import Path
# TODO: from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
@@ -25,11 +23,15 @@
if debug:
logger.info("Django is set to use a debugger. Provided debug mode: %s", debug)
if debug == "pycharm-pydevd":
- logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!")
+ logger.info(
+ "Entering debug mode for pycharm, make sure the debug server is running in PyCharm!"
+ )
import pydevd_pycharm
- pydevd_pycharm.settrace("host.docker.internal", port=56783, stdoutToServer=True, stderrToServer=True)
+ pydevd_pycharm.settrace(
+ "host.docker.internal", port=56783, stdoutToServer=True, stderrToServer=True
+ )
logger.info("Debugger started.")
elif debug == "debugpy":
@@ -43,11 +45,6 @@
else:
logger.warning("Invalid debug mode given: %s. Debugger not started", debug)
-# This allows easy placement of apps within the interior
-# scram directory.
-ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent
-sys.path.append(str(ROOT_DIR / "scram"))
-
# If DJANGO_SETTINGS_MODULE is unset, default to the local settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
diff --git a/config/consumers.py b/django/src/config/consumers.py
similarity index 92%
rename from config/consumers.py
rename to django/src/config/consumers.py
index 1b47b6e7..73efd345 100644
--- a/config/consumers.py
+++ b/django/src/config/consumers.py
@@ -33,7 +33,9 @@ def update_connect_cache():
# Filter WebSocketSequenceElements by actiontype
elements = await database_sync_to_async(list)(
- WebSocketSequenceElement.objects.filter(action_type__name=self.actiontype).order_by("order_num"),
+ WebSocketSequenceElement.objects.filter(
+ action_type__name=self.actiontype
+ ).order_by("order_num"),
)
if not elements:
logger.warning("No elements found for actiontype=%s.", self.actiontype)
@@ -46,7 +48,9 @@ def update_connect_cache():
for route in routes:
for element in elements:
- msg = await database_sync_to_async(lambda e: e.websocketmessage)(element)
+ msg = await database_sync_to_async(lambda e: e.websocketmessage)(
+ element
+ )
msg.msg_data[msg.msg_data_route_field] = str(route)
await self.send_json({"type": msg.msg_type, "message": msg.msg_data})
@@ -76,7 +80,12 @@ async def receive_json(self, content):
"last_seen": time.time(),
}
cache_key = f"translator_stats:{self.actiontype}"
- logger.info("Received heartbeat for %s: %s (Key: %s)", self.actiontype, stats, cache_key)
+ logger.info(
+ "Received heartbeat for %s: %s (Key: %s)",
+ self.actiontype,
+ stats,
+ cache_key,
+ )
await database_sync_to_async(cache.set)(cache_key, stats, timeout=300)
elif content["type"] == "translator_check_resp":
# We received a check response from a translator, forward to web UI.
diff --git a/django/src/config/routing.py b/django/src/config/routing.py
new file mode 100644
index 00000000..9b1d1d2c
--- /dev/null
+++ b/django/src/config/routing.py
@@ -0,0 +1,16 @@
+"""Define URLs for the WebSocket consumers."""
+
+from django.urls import re_path
+
+from . import consumers
+
+websocket_urlpatterns = [
+ re_path(
+ r"ws/route_manager/translator_(?P\w+)/$",
+ consumers.TranslatorConsumer.as_asgi(),
+ ),
+ re_path(
+ r"ws/route_manager/webui_(?P\w+)/$",
+ consumers.WebUIConsumer.as_asgi(),
+ ),
+]
diff --git a/config/settings/__init__.py b/django/src/config/settings/__init__.py
similarity index 100%
rename from config/settings/__init__.py
rename to django/src/config/settings/__init__.py
diff --git a/config/settings/base.py b/django/src/config/settings/base.py
similarity index 94%
rename from config/settings/base.py
rename to django/src/config/settings/base.py
index f60695be..71cf94f8 100644
--- a/config/settings/base.py
+++ b/django/src/config/settings/base.py
@@ -9,9 +9,8 @@
logger = logging.getLogger(__name__)
-ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
-# scram/
-APPS_DIR = ROOT_DIR / "scram"
+ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent.parent
+APPS_DIR = ROOT_DIR / "src" / "scram"
env = environ.Env()
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False)
@@ -112,7 +111,9 @@
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
- {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
+ },
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
@@ -217,7 +218,9 @@
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
-EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend")
+EMAIL_BACKEND = env(
+ "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
+)
# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout
EMAIL_TIMEOUT = 5
@@ -239,7 +242,9 @@
"version": 1,
"disable_existing_loggers": False,
"formatters": {
- "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"},
+ "verbose": {
+ "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"
+ },
},
"handlers": {
"console": {
@@ -258,7 +263,9 @@
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"expiry": 86400 * 7, # expire messages after a week (default 60s)
- "group_expiry": 86400 * 365 * 10, # effectively disable removing from a group (default 1d)
+ "group_expiry": 86400
+ * 365
+ * 10, # effectively disable removing from a group (default 1d)
"hosts": [(os.environ.get("REDIS_HOST", "redis"), 6379)],
},
},
@@ -317,7 +324,9 @@
MIDDLEWARE += ["mozilla_django_oidc.middleware.SessionRefresh"]
# Extend middleware to add OIDC auth backend
- AUTHENTICATION_BACKENDS += ["scram.route_manager.authentication_backends.ESnetAuthBackend"]
+ AUTHENTICATION_BACKENDS += [
+ "scram.route_manager.authentication_backends.ESnetAuthBackend"
+ ]
# https://docs.djangoproject.com/en/dev/ref/settings/#login-url
LOGIN_URL = "oidc_authentication_init"
@@ -350,7 +359,9 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#logout-url
LOGOUT_URL = "local_auth:logout"
else:
- msg = f"Invalid authentication method: {AUTH_METHOD}. Please choose 'local' or 'oidc'"
+ msg = (
+ f"Invalid authentication method: {AUTH_METHOD}. Please choose 'local' or 'oidc'"
+ )
raise ValueError(msg)
@@ -378,7 +389,12 @@
SCRAM_DENIED_GROUPS = ["svc_scram_denied"]
# This is the set of all the groups
-SCRAM_GROUPS = SCRAM_ADMIN_GROUPS + SCRAM_READWRITE_GROUPS + SCRAM_READONLY_GROUPS + SCRAM_DENIED_GROUPS
+SCRAM_GROUPS = (
+ SCRAM_ADMIN_GROUPS
+ + SCRAM_READWRITE_GROUPS
+ + SCRAM_READONLY_GROUPS
+ + SCRAM_DENIED_GROUPS
+)
# How many entries to show PER Actiontype on the home page
RECENT_LIMIT = 10
diff --git a/config/settings/local.py b/django/src/config/settings/local.py
similarity index 94%
rename from config/settings/local.py
rename to django/src/config/settings/local.py
index 5e4d7802..6ee28f09 100644
--- a/config/settings/local.py
+++ b/django/src/config/settings/local.py
@@ -30,7 +30,9 @@
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
-EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend")
+EMAIL_BACKEND = env(
+ "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
+)
# WhiteNoise
# ------------------------------------------------------------------------------
@@ -70,7 +72,9 @@
# Your stuff...
# ------------------------------------------------------------------------------
-REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.IsAdminUser",)
+REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = (
+ "rest_framework.permissions.IsAdminUser",
+)
# Behave Django testing framework
INSTALLED_APPS += ["behave_django"]
diff --git a/config/settings/production.py b/django/src/config/settings/production.py
similarity index 94%
rename from config/settings/production.py
rename to django/src/config/settings/production.py
index c1592ab0..019a0dae 100644
--- a/config/settings/production.py
+++ b/django/src/config/settings/production.py
@@ -54,11 +54,15 @@
# TODO: set this to 60 seconds first and then to 518400 once you prove the former works
SECURE_HSTS_SECONDS = 60
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains
-SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True)
+SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
+ "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True
+)
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True)
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
-SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True)
+SECURE_CONTENT_TYPE_NOSNIFF = env.bool(
+ "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True
+)
# STATIC
# ------------------------
@@ -116,7 +120,9 @@
"disable_existing_loggers": False,
"filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
"formatters": {
- "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"},
+ "verbose": {
+ "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"
+ },
},
"handlers": {
"mail_admins": {
diff --git a/config/settings/test.py b/django/src/config/settings/test.py
similarity index 80%
rename from config/settings/test.py
rename to django/src/config/settings/test.py
index e1201960..eb1ce936 100644
--- a/config/settings/test.py
+++ b/django/src/config/settings/test.py
@@ -41,10 +41,18 @@
# Your stuff...
# ------------------------------------------------------------------------------
# These variables are required by the ESnetAuthBackend called in our OidcTest case
-OIDC_OP_JWKS_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/certs"
-OIDC_OP_AUTHORIZATION_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/auth"
-OIDC_OP_TOKEN_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/token"
-OIDC_OP_USER_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/userinfo"
+OIDC_OP_JWKS_ENDPOINT = (
+ "https://example.com/auth/realms/example/protocol/openid-connect/certs"
+)
+OIDC_OP_AUTHORIZATION_ENDPOINT = (
+ "https://example.com/auth/realms/example/protocol/openid-connect/auth"
+)
+OIDC_OP_TOKEN_ENDPOINT = (
+ "https://example.com/auth/realms/example/protocol/openid-connect/token"
+)
+OIDC_OP_USER_ENDPOINT = (
+ "https://example.com/auth/realms/example/protocol/openid-connect/userinfo"
+)
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_RP_CLIENT_ID = ""
OIDC_RP_CLIENT_SECRET = ""
diff --git a/config/urls.py b/django/src/config/urls.py
similarity index 85%
rename from config/urls.py
rename to django/src/config/urls.py
index 41a45d12..b02f6e7f 100644
--- a/config/urls.py
+++ b/django/src/config/urls.py
@@ -6,7 +6,11 @@
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path
from django.views import defaults as default_views
-from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
+from drf_spectacular.views import (
+ SpectacularAPIView,
+ SpectacularRedocView,
+ SpectacularSwaggerView,
+)
from rest_framework.authtoken.views import obtain_auth_token
from .api_router import app_name
@@ -33,7 +37,9 @@
urlpatterns += [path("oidc/", include("mozilla_django_oidc.urls"))]
elif settings.AUTH_METHOD == "local":
- urlpatterns += [path("auth/", include("scram.local_auth.urls", namespace="local_auth"))]
+ urlpatterns += [
+ path("auth/", include("scram.local_auth.urls", namespace="local_auth"))
+ ]
# API URLS
api_version_urls = (
[
@@ -51,8 +57,14 @@
# Swagger OpenAPI URLs
urlpatterns += [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
- path("schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
- path("schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
+ path(
+ "schema/swagger-ui/",
+ SpectacularSwaggerView.as_view(url_name="schema"),
+ name="swagger-ui",
+ ),
+ path(
+ "schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"
+ ),
]
if settings.DEBUG:
diff --git a/config/websocket.py b/django/src/config/websocket.py
similarity index 100%
rename from config/websocket.py
rename to django/src/config/websocket.py
diff --git a/config/wsgi.py b/django/src/config/wsgi.py
similarity index 86%
rename from config/wsgi.py
rename to django/src/config/wsgi.py
index 1ebc25c0..9d10a91b 100644
--- a/config/wsgi.py
+++ b/django/src/config/wsgi.py
@@ -14,16 +14,9 @@
"""
import os
-import sys
-from pathlib import Path
from django.core.wsgi import get_wsgi_application
-# This allows easy placement of apps within the interior
-# scram directory.
-
-ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent
-sys.path.append(str(ROOT_DIR / "scram"))
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
diff --git a/manage.py b/django/src/manage.py
similarity index 77%
rename from manage.py
rename to django/src/manage.py
index cbc80d8d..1ecbd66d 100755
--- a/manage.py
+++ b/django/src/manage.py
@@ -3,7 +3,6 @@
import os
import sys
-from pathlib import Path
def main():
@@ -21,11 +20,6 @@ def main():
msg,
) from exc
- # This allows easy placement of apps within the interior
- # scram directory.
- current_path = Path(__file__).parent.resolve()
- sys.path.append(str(current_path / "scram"))
-
execute_from_command_line(sys.argv)
diff --git a/django/src/scram/__init__.py b/django/src/scram/__init__.py
new file mode 100644
index 00000000..bf9962a9
--- /dev/null
+++ b/django/src/scram/__init__.py
@@ -0,0 +1,7 @@
+"""The Django project for Security Catch and Release Automation Manager (SCRAM)."""
+
+__version__ = "1.5.1"
+__version_info__ = tuple(
+ int(num) if num.isdigit() else num
+ for num in __version__.replace("-", ".", 1).split(".")
+) # noqa: RUF067
diff --git a/scram/conftest.py b/django/src/scram/conftest.py
similarity index 100%
rename from scram/conftest.py
rename to django/src/scram/conftest.py
diff --git a/scram/contrib/__init__.py b/django/src/scram/contrib/__init__.py
similarity index 100%
rename from scram/contrib/__init__.py
rename to django/src/scram/contrib/__init__.py
diff --git a/scram/contrib/sites/__init__.py b/django/src/scram/contrib/sites/__init__.py
similarity index 100%
rename from scram/contrib/sites/__init__.py
rename to django/src/scram/contrib/sites/__init__.py
diff --git a/scram/contrib/sites/migrations/0001_initial.py b/django/src/scram/contrib/sites/migrations/0001_initial.py
similarity index 100%
rename from scram/contrib/sites/migrations/0001_initial.py
rename to django/src/scram/contrib/sites/migrations/0001_initial.py
diff --git a/scram/contrib/sites/migrations/0002_alter_domain_unique.py b/django/src/scram/contrib/sites/migrations/0002_alter_domain_unique.py
similarity index 100%
rename from scram/contrib/sites/migrations/0002_alter_domain_unique.py
rename to django/src/scram/contrib/sites/migrations/0002_alter_domain_unique.py
diff --git a/scram/contrib/sites/migrations/0003_set_site_domain_and_name.py b/django/src/scram/contrib/sites/migrations/0003_set_site_domain_and_name.py
similarity index 100%
rename from scram/contrib/sites/migrations/0003_set_site_domain_and_name.py
rename to django/src/scram/contrib/sites/migrations/0003_set_site_domain_and_name.py
diff --git a/scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py b/django/src/scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py
similarity index 100%
rename from scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py
rename to django/src/scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py
diff --git a/scram/contrib/sites/migrations/__init__.py b/django/src/scram/contrib/sites/migrations/__init__.py
similarity index 100%
rename from scram/contrib/sites/migrations/__init__.py
rename to django/src/scram/contrib/sites/migrations/__init__.py
diff --git a/scram/local_auth/__init__.py b/django/src/scram/local_auth/__init__.py
similarity index 100%
rename from scram/local_auth/__init__.py
rename to django/src/scram/local_auth/__init__.py
diff --git a/scram/local_auth/urls.py b/django/src/scram/local_auth/urls.py
similarity index 73%
rename from scram/local_auth/urls.py
rename to django/src/scram/local_auth/urls.py
index 56c7b727..69094a2a 100644
--- a/scram/local_auth/urls.py
+++ b/django/src/scram/local_auth/urls.py
@@ -8,7 +8,9 @@
urlpatterns = [
path(
"login/",
- LoginView.as_view(template_name="local_auth/login.html", success_url="route_manager:home"),
+ LoginView.as_view(
+ template_name="local_auth/login.html", success_url="route_manager:home"
+ ),
name="login",
),
path("logout/", LogoutView.as_view(), name="logout"),
diff --git a/scram/route_manager/__init__.py b/django/src/scram/route_manager/__init__.py
similarity index 100%
rename from scram/route_manager/__init__.py
rename to django/src/scram/route_manager/__init__.py
diff --git a/scram/route_manager/admin.py b/django/src/scram/route_manager/admin.py
similarity index 100%
rename from scram/route_manager/admin.py
rename to django/src/scram/route_manager/admin.py
diff --git a/scram/route_manager/api/__init__.py b/django/src/scram/route_manager/api/__init__.py
similarity index 100%
rename from scram/route_manager/api/__init__.py
rename to django/src/scram/route_manager/api/__init__.py
diff --git a/scram/route_manager/api/exceptions.py b/django/src/scram/route_manager/api/exceptions.py
similarity index 92%
rename from scram/route_manager/api/exceptions.py
rename to django/src/scram/route_manager/api/exceptions.py
index 297b4477..1793101a 100644
--- a/scram/route_manager/api/exceptions.py
+++ b/django/src/scram/route_manager/api/exceptions.py
@@ -18,7 +18,9 @@ class IgnoredRoute(APIException):
"""An operation attempted to add a route that overlaps with a route on the ignore list."""
status_code = 400
- default_detail = "This CIDR is on the ignore list. You are not allowed to add it here."
+ default_detail = (
+ "This CIDR is on the ignore list. You are not allowed to add it here."
+ )
default_code = "ignored_route"
diff --git a/scram/route_manager/api/serializers.py b/django/src/scram/route_manager/api/serializers.py
similarity index 97%
rename from scram/route_manager/api/serializers.py
rename to django/src/scram/route_manager/api/serializers.py
index be12bca4..10e4eafe 100644
--- a/scram/route_manager/api/serializers.py
+++ b/django/src/scram/route_manager/api/serializers.py
@@ -82,7 +82,9 @@ class EntrySerializer(serializers.HyperlinkedModelSerializer):
else:
who = serializers.CharField()
comment = serializers.CharField()
- originating_scram_instance = serializers.CharField(default="scram_hostname_not_set", read_only=True)
+ originating_scram_instance = serializers.CharField(
+ default="scram_hostname_not_set", read_only=True
+ )
is_active = serializers.BooleanField(default=True, read_only=True)
def __init__(self, *args, **kwargs):
diff --git a/scram/route_manager/api/views.py b/django/src/scram/route_manager/api/views.py
similarity index 80%
rename from scram/route_manager/api/views.py
rename to django/src/scram/route_manager/api/views.py
index 51ee4f89..11c6080a 100644
--- a/scram/route_manager/api/views.py
+++ b/django/src/scram/route_manager/api/views.py
@@ -23,8 +23,21 @@
from scram import __version__ as scram_version
-from ..models import ActionType, Client, Entry, IgnoreEntry, Route, WebSocketSequenceElement
-from .exceptions import ActiontypeNotAllowed, IgnoredRoute, NoActiveEntryFound, PrefixTooLarge
+
+from ..models import (
+ ActionType,
+ Client,
+ Entry,
+ IgnoreEntry,
+ Route,
+ WebSocketSequenceElement,
+)
+from .exceptions import (
+ ActiontypeNotAllowed,
+ IgnoredRoute,
+ NoActiveEntryFound,
+ PrefixTooLarge,
+)
from .serializers import (
ActionTypeSerializer,
ClientSerializer,
@@ -40,8 +53,13 @@
@extend_schema(
description="API endpoint for actiontypes.",
responses={
- 200: OpenApiResponse(response=ActionTypeSerializer, description="Successful retrieval of actiontype(s)."),
- 403: OpenApiResponse(description="Authentication credentials were not provided."),
+ 200: OpenApiResponse(
+ response=ActionTypeSerializer,
+ description="Successful retrieval of actiontype(s).",
+ ),
+ 403: OpenApiResponse(
+ description="Authentication credentials were not provided."
+ ),
404: OpenApiResponse(description="The requested actiontype does not exist."),
},
)
@@ -58,12 +76,18 @@ class ActionTypeViewSet(viewsets.ReadOnlyModelViewSet):
description="API endpoint for ignore entries.",
responses={
200: OpenApiResponse(
- response=IgnoreEntrySerializer, description="Successful retrieval or update of an ignore entry."
+ response=IgnoreEntrySerializer,
+ description="Successful retrieval or update of an ignore entry.",
+ ),
+ 201: OpenApiResponse(
+ response=IgnoreEntrySerializer,
+ description="Ignore entry successfully created.",
),
- 201: OpenApiResponse(response=IgnoreEntrySerializer, description="Ignore entry successfully created."),
204: OpenApiResponse(description="Ignore entry successfully deleted."),
400: OpenApiResponse(description="Invalid data provided."),
- 403: OpenApiResponse(description="Authentication credentials were not provided."),
+ 403: OpenApiResponse(
+ description="Authentication credentials were not provided."
+ ),
404: OpenApiResponse(description="The requested ignore entry does not exist."),
},
)
@@ -83,7 +107,9 @@ class IgnoreEntryViewSet(viewsets.ModelViewSet):
response=ClientSerializer,
description="Client already existed and was retrieved successfully.",
),
- 201: OpenApiResponse(response=ClientSerializer, description="Client successfully created."),
+ 201: OpenApiResponse(
+ response=ClientSerializer, description="Client successfully created."
+ ),
400: OpenApiResponse(
description="Client with this name already exists with a different UUID, or client_name was not provided."
),
@@ -105,7 +131,10 @@ def create(self, request, *args, **kwargs):
request_uuid = request.data.get("uuid")
if not client_name:
- return Response({"detail": "client_name is required."}, status=status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {"detail": "client_name is required."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
existing_client = self.queryset.filter(client_name=client_name).first()
@@ -141,7 +170,8 @@ def create(self, request, *args, **kwargs):
],
responses={
200: OpenApiResponse(
- response=IsActiveSerializer, description="The 'is_active' field indicates the status of the route."
+ response=IsActiveSerializer,
+ description="The 'is_active' field indicates the status of the route.",
),
400: OpenApiResponse(description="The 'cidr' parameter is missing or invalid."),
},
@@ -164,25 +194,34 @@ def get_queryset(self):
try:
normalized_cidr = ipaddress.ip_network(cidr, strict=False)
except ValueError:
- raise ValidationError(detail={"error": "invalid ip address or network"}) from None
+ raise ValidationError(
+ detail={"error": "invalid ip address or network"}
+ ) from None
self.normalization_warning = None
self.normalized_cidr_for_response = normalized_cidr
if str(cidr) != str(normalized_cidr):
# save the warning so we can use it in the list response
- self.normalization_warning = (
- f"Input CIDR '{cidr}' was not canonical and was normalized to '{normalized_cidr!s}' for the search."
- )
+ self.normalization_warning = f"Input CIDR '{cidr}' was not canonical and was normalized to '{normalized_cidr!s}' for the search."
- return Entry.objects.filter(route__route__net_contained_or_equal=normalized_cidr, is_active=True)
+ return Entry.objects.filter(
+ route__route__net_contained_or_equal=normalized_cidr, is_active=True
+ )
def list(self, request):
"""Override the list function to just return a boolean instead of other metadata."""
queryset = self.get_queryset()
if not queryset.exists() and hasattr(self, "normalized_cidr_for_response"):
- response_data = {"results": [{"is_active": False, "route": str(self.normalized_cidr_for_response)}]}
+ response_data = {
+ "results": [
+ {
+ "is_active": False,
+ "route": str(self.normalized_cidr_for_response),
+ }
+ ]
+ }
else:
serializer = self.get_serializer(queryset, many=True)
response_data = {"results": serializer.data}
@@ -194,11 +233,20 @@ def list(self, request):
@extend_schema(
description="API endpoint for entries.",
responses={
- 200: OpenApiResponse(response=EntrySerializer, description="Successful retrieval of an entry/entries."),
- 201: OpenApiResponse(response=EntrySerializer, description="Entry successfully created."),
+ 200: OpenApiResponse(
+ response=EntrySerializer,
+ description="Successful retrieval of an entry/entries.",
+ ),
+ 201: OpenApiResponse(
+ response=EntrySerializer, description="Entry successfully created."
+ ),
204: OpenApiResponse(description="Entry successfully deleted."),
- 400: OpenApiResponse(description="The route is likely on the ignore list or the prefix is too large."),
- 403: OpenApiResponse(description="The client is not authorized for this action."),
+ 400: OpenApiResponse(
+ description="The route is likely on the ignore list or the prefix is too large."
+ ),
+ 403: OpenApiResponse(
+ description="The client is not authorized for this action."
+ ),
404: OpenApiResponse(description="The requested entry does not exist."),
},
)
@@ -232,15 +280,21 @@ def check_client_authorization(self, actiontype):
raise PermissionDenied(msg) from client_dne
# Check if client is authorized for the action type
- if not client.is_authorized or actiontype not in client.authorized_actiontypes.values_list(
- "name", flat=True
+ if (
+ not client.is_authorized
+ or actiontype
+ not in client.authorized_actiontypes.values_list("name", flat=True)
):
logger.debug(
"Client: %s, actiontypes: %s",
uuid,
list(client.authorized_actiontypes.values_list("name", flat=True)),
)
- logger.info("%s is not allowed to add an entry to the %s list.", uuid, actiontype)
+ logger.info(
+ "%s is not allowed to add an entry to the %s list.",
+ uuid,
+ actiontype,
+ )
raise ActiontypeNotAllowed
elif not self.request.user.has_perm("route_manager.can_add_entry"):
@@ -251,8 +305,15 @@ def check_ignore_list(route):
"""Ensure that we're not trying to block something from the ignore list."""
overlapping_ignore = IgnoreEntry.objects.filter(route__net_overlaps=route)
if overlapping_ignore.count():
- ignore_entries = [str(ignore_entry["route"]) for ignore_entry in overlapping_ignore.values()]
- logger.info("Cannot proceed adding %s. The ignore list contains %s.", route, ignore_entries)
+ ignore_entries = [
+ str(ignore_entry["route"])
+ for ignore_entry in overlapping_ignore.values()
+ ]
+ logger.info(
+ "Cannot proceed adding %s. The ignore list contains %s.",
+ route,
+ ignore_entries,
+ )
raise IgnoredRoute
def perform_create(self, serializer):
@@ -279,7 +340,9 @@ def perform_create(self, serializer):
self.check_client_authorization(actiontype)
self.check_ignore_list(route_instance)
- elements = WebSocketSequenceElement.objects.filter(action_type__name=actiontype).order_by("order_num")
+ elements = WebSocketSequenceElement.objects.filter(
+ action_type__name=actiontype
+ ).order_by("order_num")
if not elements:
logger.warning("No elements found for actiontype: %s", actiontype)
@@ -317,7 +380,10 @@ def perform_update(self, serializer):
msg = "You can only update your own entries"
raise PermissionDenied(msg)
- serializer.save(who=serializer.instance.who, originating_scram_instance=settings.SCRAM_HOSTNAME)
+ serializer.save(
+ who=serializer.instance.who,
+ originating_scram_instance=settings.SCRAM_HOSTNAME,
+ )
entry = serializer.instance
update_change_reason(entry, comment)
@@ -418,8 +484,14 @@ def _get_translator_stats() -> dict[str, dict]:
# Filter out stale heartbeats (e.g., > 90s)
now = time.time()
active_bgp_stat = {"v4": 0, "v6": 0}
- if bgp_stats and now - bgp_stats["last_seen"] < translator_heartbeat_timeout:
- active_bgp_stat = {"v4": bgp_stats["v4_count"], "v6": bgp_stats["v6_count"]}
+ if (
+ bgp_stats
+ and now - bgp_stats["last_seen"] < translator_heartbeat_timeout
+ ):
+ active_bgp_stat = {
+ "v4": bgp_stats["v4_count"],
+ "v6": bgp_stats["v6_count"],
+ }
translator_stats[at.name] = {
"count": count,
@@ -436,8 +508,7 @@ def _get_entries_stats() -> tuple[int, int]:
entries_stats = {}
try:
counts = (
- Entry.objects
- .filter(is_active=True)
+ Entry.objects.filter(is_active=True)
.values("actiontype__name")
.annotate(count=Count("id"))
.order_by("actiontype__name")
@@ -460,7 +531,11 @@ def get(request):
if "error" in value:
overall_status = "unhealthy"
- http_status = status.HTTP_200_OK if overall_status == "healthy" else status.HTTP_503_SERVICE_UNAVAILABLE
+ http_status = (
+ status.HTTP_200_OK
+ if overall_status == "healthy"
+ else status.HTTP_503_SERVICE_UNAVAILABLE
+ )
return Response(
{
diff --git a/scram/route_manager/apps.py b/django/src/scram/route_manager/apps.py
similarity index 100%
rename from scram/route_manager/apps.py
rename to django/src/scram/route_manager/apps.py
diff --git a/scram/route_manager/authentication_backends.py b/django/src/scram/route_manager/authentication_backends.py
similarity index 100%
rename from scram/route_manager/authentication_backends.py
rename to django/src/scram/route_manager/authentication_backends.py
diff --git a/scram/route_manager/context_processors.py b/django/src/scram/route_manager/context_processors.py
similarity index 85%
rename from scram/route_manager/context_processors.py
rename to django/src/scram/route_manager/context_processors.py
index e08a2fc5..89768dd9 100644
--- a/scram/route_manager/context_processors.py
+++ b/django/src/scram/route_manager/context_processors.py
@@ -27,5 +27,8 @@ def active_count(request):
if "admin" not in request.META["PATH_INFO"]:
active_block_entries = Entry.objects.filter(is_active=True).count()
total_block_entries = Entry.objects.all().count()
- return {"active_block_entries": active_block_entries, "total_block_entries": total_block_entries}
+ return {
+ "active_block_entries": active_block_entries,
+ "total_block_entries": total_block_entries,
+ }
return {}
diff --git a/scram/route_manager/migrations/0001_initial.py b/django/src/scram/route_manager/migrations/0001_initial.py
similarity index 100%
rename from scram/route_manager/migrations/0001_initial.py
rename to django/src/scram/route_manager/migrations/0001_initial.py
diff --git a/scram/route_manager/migrations/0002_ipaddress_uuid.py b/django/src/scram/route_manager/migrations/0002_ipaddress_uuid.py
similarity index 100%
rename from scram/route_manager/migrations/0002_ipaddress_uuid.py
rename to django/src/scram/route_manager/migrations/0002_ipaddress_uuid.py
diff --git a/scram/route_manager/migrations/0003_auto_20210408_0413.py b/django/src/scram/route_manager/migrations/0003_auto_20210408_0413.py
similarity index 100%
rename from scram/route_manager/migrations/0003_auto_20210408_0413.py
rename to django/src/scram/route_manager/migrations/0003_auto_20210408_0413.py
diff --git a/scram/route_manager/migrations/0004_actiontype.py b/django/src/scram/route_manager/migrations/0004_actiontype.py
similarity index 100%
rename from scram/route_manager/migrations/0004_actiontype.py
rename to django/src/scram/route_manager/migrations/0004_actiontype.py
diff --git a/scram/route_manager/migrations/0005_entry.py b/django/src/scram/route_manager/migrations/0005_entry.py
similarity index 100%
rename from scram/route_manager/migrations/0005_entry.py
rename to django/src/scram/route_manager/migrations/0005_entry.py
diff --git a/scram/route_manager/migrations/0006_history.py b/django/src/scram/route_manager/migrations/0006_history.py
similarity index 100%
rename from scram/route_manager/migrations/0006_history.py
rename to django/src/scram/route_manager/migrations/0006_history.py
diff --git a/scram/route_manager/migrations/0007_history_expiration.py b/django/src/scram/route_manager/migrations/0007_history_expiration.py
similarity index 100%
rename from scram/route_manager/migrations/0007_history_expiration.py
rename to django/src/scram/route_manager/migrations/0007_history_expiration.py
diff --git a/scram/route_manager/migrations/0008_default_block_actiontype.py b/django/src/scram/route_manager/migrations/0008_default_block_actiontype.py
similarity index 100%
rename from scram/route_manager/migrations/0008_default_block_actiontype.py
rename to django/src/scram/route_manager/migrations/0008_default_block_actiontype.py
diff --git a/scram/route_manager/migrations/0009_expiration_to_datetime.py b/django/src/scram/route_manager/migrations/0009_expiration_to_datetime.py
similarity index 100%
rename from scram/route_manager/migrations/0009_expiration_to_datetime.py
rename to django/src/scram/route_manager/migrations/0009_expiration_to_datetime.py
diff --git a/scram/route_manager/migrations/0010_actiontype_helptext.py b/django/src/scram/route_manager/migrations/0010_actiontype_helptext.py
similarity index 100%
rename from scram/route_manager/migrations/0010_actiontype_helptext.py
rename to django/src/scram/route_manager/migrations/0010_actiontype_helptext.py
diff --git a/scram/route_manager/migrations/0011_history_helptext.py b/django/src/scram/route_manager/migrations/0011_history_helptext.py
similarity index 100%
rename from scram/route_manager/migrations/0011_history_helptext.py
rename to django/src/scram/route_manager/migrations/0011_history_helptext.py
diff --git a/scram/route_manager/migrations/0012_unique_entries.py b/django/src/scram/route_manager/migrations/0012_unique_entries.py
similarity index 100%
rename from scram/route_manager/migrations/0012_unique_entries.py
rename to django/src/scram/route_manager/migrations/0012_unique_entries.py
diff --git a/scram/route_manager/migrations/0013_accept_cidrs.py b/django/src/scram/route_manager/migrations/0013_accept_cidrs.py
similarity index 100%
rename from scram/route_manager/migrations/0013_accept_cidrs.py
rename to django/src/scram/route_manager/migrations/0013_accept_cidrs.py
diff --git a/scram/route_manager/migrations/0014_create_groups.py b/django/src/scram/route_manager/migrations/0014_create_groups.py
similarity index 100%
rename from scram/route_manager/migrations/0014_create_groups.py
rename to django/src/scram/route_manager/migrations/0014_create_groups.py
diff --git a/scram/route_manager/migrations/0015_entry_is_active.py b/django/src/scram/route_manager/migrations/0015_entry_is_active.py
similarity index 100%
rename from scram/route_manager/migrations/0015_entry_is_active.py
rename to django/src/scram/route_manager/migrations/0015_entry_is_active.py
diff --git a/scram/route_manager/migrations/0016_auto_20211202_1933.py b/django/src/scram/route_manager/migrations/0016_auto_20211202_1933.py
similarity index 100%
rename from scram/route_manager/migrations/0016_auto_20211202_1933.py
rename to django/src/scram/route_manager/migrations/0016_auto_20211202_1933.py
diff --git a/scram/route_manager/migrations/0017_ignorelist.py b/django/src/scram/route_manager/migrations/0017_ignorelist.py
similarity index 100%
rename from scram/route_manager/migrations/0017_ignorelist.py
rename to django/src/scram/route_manager/migrations/0017_ignorelist.py
diff --git a/scram/route_manager/migrations/0018_auto_20220702_0203.py b/django/src/scram/route_manager/migrations/0018_auto_20220702_0203.py
similarity index 100%
rename from scram/route_manager/migrations/0018_auto_20220702_0203.py
rename to django/src/scram/route_manager/migrations/0018_auto_20220702_0203.py
diff --git a/scram/route_manager/migrations/0019_auto_20220708_1945.py b/django/src/scram/route_manager/migrations/0019_auto_20220708_1945.py
similarity index 100%
rename from scram/route_manager/migrations/0019_auto_20220708_1945.py
rename to django/src/scram/route_manager/migrations/0019_auto_20220708_1945.py
diff --git a/scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py b/django/src/scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py
similarity index 100%
rename from scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py
rename to django/src/scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py
diff --git a/scram/route_manager/migrations/0021_auto_20220929_2047.py b/django/src/scram/route_manager/migrations/0021_auto_20220929_2047.py
similarity index 100%
rename from scram/route_manager/migrations/0021_auto_20220929_2047.py
rename to django/src/scram/route_manager/migrations/0021_auto_20220929_2047.py
diff --git a/scram/route_manager/migrations/0022_auto_20230117_1930.py b/django/src/scram/route_manager/migrations/0022_auto_20230117_1930.py
similarity index 100%
rename from scram/route_manager/migrations/0022_auto_20230117_1930.py
rename to django/src/scram/route_manager/migrations/0022_auto_20230117_1930.py
diff --git a/scram/route_manager/migrations/0023_client.py b/django/src/scram/route_manager/migrations/0023_client.py
similarity index 100%
rename from scram/route_manager/migrations/0023_client.py
rename to django/src/scram/route_manager/migrations/0023_client.py
diff --git a/scram/route_manager/migrations/0024_alter_client_is_authorized.py b/django/src/scram/route_manager/migrations/0024_alter_client_is_authorized.py
similarity index 100%
rename from scram/route_manager/migrations/0024_alter_client_is_authorized.py
rename to django/src/scram/route_manager/migrations/0024_alter_client_is_authorized.py
diff --git a/scram/route_manager/migrations/0025_rename_uuid_client_uuid.py b/django/src/scram/route_manager/migrations/0025_rename_uuid_client_uuid.py
similarity index 100%
rename from scram/route_manager/migrations/0025_rename_uuid_client_uuid.py
rename to django/src/scram/route_manager/migrations/0025_rename_uuid_client_uuid.py
diff --git a/scram/route_manager/migrations/0026_alter_client_hostname.py b/django/src/scram/route_manager/migrations/0026_alter_client_hostname.py
similarity index 100%
rename from scram/route_manager/migrations/0026_alter_client_hostname.py
rename to django/src/scram/route_manager/migrations/0026_alter_client_hostname.py
diff --git a/scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py b/django/src/scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py
similarity index 100%
rename from scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py
rename to django/src/scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py
diff --git a/scram/route_manager/migrations/0028_default_websocket_messages.py b/django/src/scram/route_manager/migrations/0028_default_websocket_messages.py
similarity index 100%
rename from scram/route_manager/migrations/0028_default_websocket_messages.py
rename to django/src/scram/route_manager/migrations/0028_default_websocket_messages.py
diff --git a/scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py b/django/src/scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py
similarity index 100%
rename from scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py
rename to django/src/scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py
diff --git a/scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py b/django/src/scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py
similarity index 100%
rename from scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py
rename to django/src/scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py
diff --git a/scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py b/django/src/scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py
similarity index 100%
rename from scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py
rename to django/src/scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py
diff --git a/scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py b/django/src/scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py
similarity index 100%
rename from scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py
rename to django/src/scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py
diff --git a/scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py b/django/src/scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py
similarity index 100%
rename from scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py
rename to django/src/scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py
diff --git a/scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py b/django/src/scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py
similarity index 100%
rename from scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py
rename to django/src/scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py
diff --git a/scram/route_manager/migrations/0035_alter_client_uuid.py b/django/src/scram/route_manager/migrations/0035_alter_client_uuid.py
similarity index 100%
rename from scram/route_manager/migrations/0035_alter_client_uuid.py
rename to django/src/scram/route_manager/migrations/0035_alter_client_uuid.py
diff --git a/scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py b/django/src/scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py
similarity index 100%
rename from scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py
rename to django/src/scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py
diff --git a/scram/route_manager/migrations/0036_rename_hostname_client_client_name.py b/django/src/scram/route_manager/migrations/0036_rename_hostname_client_client_name.py
similarity index 100%
rename from scram/route_manager/migrations/0036_rename_hostname_client_client_name.py
rename to django/src/scram/route_manager/migrations/0036_rename_hostname_client_client_name.py
diff --git a/scram/route_manager/migrations/0037_unique_client_uuid.py b/django/src/scram/route_manager/migrations/0037_unique_client_uuid.py
similarity index 100%
rename from scram/route_manager/migrations/0037_unique_client_uuid.py
rename to django/src/scram/route_manager/migrations/0037_unique_client_uuid.py
diff --git a/scram/route_manager/migrations/0038_merge_20260310_2137.py b/django/src/scram/route_manager/migrations/0038_merge_20260310_2137.py
similarity index 100%
rename from scram/route_manager/migrations/0038_merge_20260310_2137.py
rename to django/src/scram/route_manager/migrations/0038_merge_20260310_2137.py
diff --git a/scram/route_manager/migrations/__init__.py b/django/src/scram/route_manager/migrations/__init__.py
similarity index 100%
rename from scram/route_manager/migrations/__init__.py
rename to django/src/scram/route_manager/migrations/__init__.py
diff --git a/scram/route_manager/models.py b/django/src/scram/route_manager/models.py
similarity index 89%
rename from scram/route_manager/models.py
rename to django/src/scram/route_manager/models.py
index 98d70b9d..caba5550 100644
--- a/scram/route_manager/models.py
+++ b/django/src/scram/route_manager/models.py
@@ -33,8 +33,12 @@ def get_absolute_url():
class ActionType(models.Model):
"""Define a type of action that can be done with a given route. e.g. Block, shunt, redirect, etc."""
- name = models.CharField(help_text="One-word description of the action", max_length=30)
- available = models.BooleanField(help_text="Is this a valid choice for new entries?", default=True)
+ name = models.CharField(
+ help_text="One-word description of the action", max_length=30
+ )
+ available = models.BooleanField(
+ help_text="Is this a valid choice for new entries?", default=True
+ )
history = HistoricalRecords()
def __str__(self):
@@ -48,7 +52,9 @@ class WebSocketMessage(models.Model):
"""Define a single message sent to downstream translators via WebSocket."""
msg_type = models.CharField("The type of the message", max_length=50)
- msg_data = models.JSONField("The JSON payload. See also msg_data_route_field.", default=dict)
+ msg_data = models.JSONField(
+ "The JSON payload. See also msg_data_route_field.", default=dict
+ )
msg_data_route_field = models.CharField(
"The key in the JSON payload whose value will contain the route being acted on.",
default="route",
@@ -98,8 +104,12 @@ class Entry(models.Model):
history = HistoricalRecords()
when = models.DateTimeField(auto_now_add=True)
who = models.CharField("Username", default="Unknown", max_length=30)
- originating_scram_instance = models.CharField(default="scram_hostname_not_set", max_length=255)
- expiration = models.DateTimeField(default=datetime.datetime(9999, 12, 31, 0, 0, tzinfo=datetime.UTC))
+ originating_scram_instance = models.CharField(
+ default="scram_hostname_not_set", max_length=255
+ )
+ expiration = models.DateTimeField(
+ default=datetime.datetime(9999, 12, 31, 0, 0, tzinfo=datetime.UTC)
+ )
expiration_reason = models.CharField(
help_text="Optional reason for the expiration",
max_length=200,
@@ -115,7 +125,9 @@ class Meta:
def __str__(self):
"""Summarize the most important fields to something easily readable."""
- desc = f"{self.route} ({self.actiontype}) from: {self.originating_scram_instance}"
+ desc = (
+ f"{self.route} ({self.actiontype}) from: {self.originating_scram_instance}"
+ )
if not self.is_active:
desc += " (inactive)"
return desc
diff --git a/scram/route_manager/tests/__init__.py b/django/src/scram/route_manager/tests/__init__.py
similarity index 100%
rename from scram/route_manager/tests/__init__.py
rename to django/src/scram/route_manager/tests/__init__.py
diff --git a/scram/route_manager/tests/acceptance/environment.py b/django/src/scram/route_manager/tests/acceptance/environment.py
similarity index 100%
rename from scram/route_manager/tests/acceptance/environment.py
rename to django/src/scram/route_manager/tests/acceptance/environment.py
diff --git a/scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature b/django/src/scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature
rename to django/src/scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature
diff --git a/scram/route_manager/tests/acceptance/features/client.feature b/django/src/scram/route_manager/tests/acceptance/features/client.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/client.feature
rename to django/src/scram/route_manager/tests/acceptance/features/client.feature
diff --git a/scram/route_manager/tests/acceptance/features/expiration.feature b/django/src/scram/route_manager/tests/acceptance/features/expiration.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/expiration.feature
rename to django/src/scram/route_manager/tests/acceptance/features/expiration.feature
diff --git a/scram/route_manager/tests/acceptance/features/ignorelist.feature b/django/src/scram/route_manager/tests/acceptance/features/ignorelist.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/ignorelist.feature
rename to django/src/scram/route_manager/tests/acceptance/features/ignorelist.feature
diff --git a/scram/route_manager/tests/acceptance/features/initial_data.feature b/django/src/scram/route_manager/tests/acceptance/features/initial_data.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/initial_data.feature
rename to django/src/scram/route_manager/tests/acceptance/features/initial_data.feature
diff --git a/scram/route_manager/tests/acceptance/features/query.feature b/django/src/scram/route_manager/tests/acceptance/features/query.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/query.feature
rename to django/src/scram/route_manager/tests/acceptance/features/query.feature
diff --git a/scram/route_manager/tests/acceptance/features/remove_ip.feature b/django/src/scram/route_manager/tests/acceptance/features/remove_ip.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/remove_ip.feature
rename to django/src/scram/route_manager/tests/acceptance/features/remove_ip.feature
diff --git a/scram/route_manager/tests/acceptance/features/restrict_changes.feature b/django/src/scram/route_manager/tests/acceptance/features/restrict_changes.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/restrict_changes.feature
rename to django/src/scram/route_manager/tests/acceptance/features/restrict_changes.feature
diff --git a/scram/route_manager/tests/acceptance/features/search.feature b/django/src/scram/route_manager/tests/acceptance/features/search.feature
similarity index 100%
rename from scram/route_manager/tests/acceptance/features/search.feature
rename to django/src/scram/route_manager/tests/acceptance/features/search.feature
diff --git a/scram/route_manager/tests/acceptance/steps/common.py b/django/src/scram/route_manager/tests/acceptance/steps/common.py
similarity index 100%
rename from scram/route_manager/tests/acceptance/steps/common.py
rename to django/src/scram/route_manager/tests/acceptance/steps/common.py
diff --git a/scram/route_manager/tests/acceptance/steps/ip.py b/django/src/scram/route_manager/tests/acceptance/steps/ip.py
similarity index 100%
rename from scram/route_manager/tests/acceptance/steps/ip.py
rename to django/src/scram/route_manager/tests/acceptance/steps/ip.py
diff --git a/scram/route_manager/tests/acceptance/steps/translator.py b/django/src/scram/route_manager/tests/acceptance/steps/translator.py
similarity index 84%
rename from scram/route_manager/tests/acceptance/steps/translator.py
rename to django/src/scram/route_manager/tests/acceptance/steps/translator.py
index ff478a9a..0ba67d2b 100644
--- a/scram/route_manager/tests/acceptance/steps/translator.py
+++ b/django/src/scram/route_manager/tests/acceptance/steps/translator.py
@@ -9,11 +9,15 @@
async def query_translator(route, actiontype, is_announced):
"""Ensure the specified route is currently either blocked or unblocked."""
- communicator = WebsocketCommunicator(ws_application, f"/ws/route_manager/webui_{actiontype}/")
+ communicator = WebsocketCommunicator(
+ ws_application, f"/ws/route_manager/webui_{actiontype}/"
+ )
connected, _ = await communicator.connect()
assert connected
- await communicator.send_json_to({"type": "wui_check_req", "message": {"route": route}})
+ await communicator.send_json_to(
+ {"type": "wui_check_req", "message": {"route": route}}
+ )
response = await communicator.receive_json_from(timeout=10)
assert response["type"] == "wui_check_resp"
assert response["message"]["is_blocked"] == is_announced
diff --git a/scram/route_manager/tests/functional_tests.py b/django/src/scram/route_manager/tests/functional_tests.py
similarity index 100%
rename from scram/route_manager/tests/functional_tests.py
rename to django/src/scram/route_manager/tests/functional_tests.py
diff --git a/scram/route_manager/tests/integration/environment.py b/django/src/scram/route_manager/tests/integration/environment.py
similarity index 100%
rename from scram/route_manager/tests/integration/environment.py
rename to django/src/scram/route_manager/tests/integration/environment.py
diff --git a/scram/route_manager/tests/integration/features/multi_instance_sync.feature b/django/src/scram/route_manager/tests/integration/features/multi_instance_sync.feature
similarity index 100%
rename from scram/route_manager/tests/integration/features/multi_instance_sync.feature
rename to django/src/scram/route_manager/tests/integration/features/multi_instance_sync.feature
diff --git a/scram/route_manager/tests/integration/steps/common.py b/django/src/scram/route_manager/tests/integration/steps/common.py
similarity index 100%
rename from scram/route_manager/tests/integration/steps/common.py
rename to django/src/scram/route_manager/tests/integration/steps/common.py
diff --git a/scram/route_manager/tests/integration/steps/ip.py b/django/src/scram/route_manager/tests/integration/steps/ip.py
similarity index 100%
rename from scram/route_manager/tests/integration/steps/ip.py
rename to django/src/scram/route_manager/tests/integration/steps/ip.py
diff --git a/scram/route_manager/tests/integration/steps/multi_instance.py b/django/src/scram/route_manager/tests/integration/steps/multi_instance.py
similarity index 91%
rename from scram/route_manager/tests/integration/steps/multi_instance.py
rename to django/src/scram/route_manager/tests/integration/steps/multi_instance.py
index ddff80aa..1695c863 100644
--- a/scram/route_manager/tests/integration/steps/multi_instance.py
+++ b/django/src/scram/route_manager/tests/integration/steps/multi_instance.py
@@ -14,7 +14,11 @@
def get_auth_token(base_url: str = DJANGO_PRIMARY_URL):
"""Obtain an API authentication token for the test user."""
- response = requests.post(f"{base_url}/auth-token/", data={"username": "user", "password": "password"}, timeout=10)
+ response = requests.post(
+ f"{base_url}/auth-token/",
+ data={"username": "user", "password": "password"},
+ timeout=10,
+ )
response.raise_for_status()
return response.json()["token"]
@@ -160,7 +164,9 @@ def check_entry_inactive_on_secondary(context, ip):
entries = list_response.json().get("results", [])
for entry in entries:
if entry.get("route") == ip:
- context.test.fail(f"Entry {ip} should be inactive but was found in active entries list")
+ context.test.fail(
+ f"Entry {ip} should be inactive but was found in active entries list"
+ )
# If we get here, the entry was not found in active entries, which is correct
except requests.exceptions.RequestException as e:
@@ -196,13 +202,19 @@ def check_announced_on_secondary(context, ip):
# Verify process_updates actually processed this specific entry
if hasattr(context, "secondary_process_data"):
- reprocessed_list = context.secondary_process_data.get("entries_reprocessed_list", [])
- secondary_hostname = context.secondary_process_data.get("scram_hostname", "UNKNOWN")
+ reprocessed_list = context.secondary_process_data.get(
+ "entries_reprocessed_list", []
+ )
+ secondary_hostname = context.secondary_process_data.get(
+ "scram_hostname", "UNKNOWN"
+ )
originating_instance = None
for entry in entries:
if entry.get("route") == ip:
- originating_instance = entry.get("originating_scram_instance", "UNKNOWN")
+ originating_instance = entry.get(
+ "originating_scram_instance", "UNKNOWN"
+ )
break
assert ip in reprocessed_list, (
f"Expected {ip} in reprocessed list, got {reprocessed_list}. "
@@ -228,14 +240,18 @@ def check_removal_announced_on_secondary(context, ip):
entries = list_response.json().get("results", [])
for entry in entries:
if entry.get("route") == ip:
- context.test.fail(f"Entry {ip} should be inactive but was found in active entries list")
+ context.test.fail(
+ f"Entry {ip} should be inactive but was found in active entries list"
+ )
# Entry is not in active list, which is correct for a removed entry
except requests.exceptions.RequestException as e:
context.test.fail(f"Failed to call API: {e}")
# Verify process_updates actually processed this specific entry
- process_data = getattr(context, "secondary_process_data", None) or getattr(context, "primary_process_data", {})
+ process_data = getattr(context, "secondary_process_data", None) or getattr(
+ context, "primary_process_data", {}
+ )
if process_data:
reprocessed_list = process_data.get("entries_reprocessed_list", [])
hostname = process_data.get("scram_hostname", "UNKNOWN")
@@ -261,7 +277,9 @@ def check_removal_announced_on_primary(context, ip):
entries = list_response.json().get("results", [])
for entry in entries:
if entry.get("route") == ip:
- context.test.fail(f"Entry {ip} should be inactive but was found in active entries list")
+ context.test.fail(
+ f"Entry {ip} should be inactive but was found in active entries list"
+ )
except requests.exceptions.RequestException as e:
context.test.fail(f"Failed to call API: {e}")
diff --git a/scram/route_manager/tests/test_admin.py b/django/src/scram/route_manager/tests/test_admin.py
similarity index 75%
rename from scram/route_manager/tests/test_admin.py
rename to django/src/scram/route_manager/tests/test_admin.py
index 5063fc94..e9ddcad7 100644
--- a/scram/route_manager/tests/test_admin.py
+++ b/django/src/scram/route_manager/tests/test_admin.py
@@ -17,12 +17,18 @@ def setUp(self):
route1 = Route.objects.create(route="192.168.1.1")
route2 = Route.objects.create(route="192.168.1.2")
- self.entry1 = Entry.objects.create(route=route1, actiontype=self.atype, who="admin")
- self.entry2 = Entry.objects.create(route=route2, actiontype=self.atype, who="user1")
+ self.entry1 = Entry.objects.create(
+ route=route1, actiontype=self.atype, who="admin"
+ )
+ self.entry2 = Entry.objects.create(
+ route=route2, actiontype=self.atype, who="user1"
+ )
def test_who_filter_lookups(self):
"""Test that the WhoFilter returns the correct users who have made entries."""
- who_filter = WhoFilter(request=None, params={}, model=Entry, model_admin=EntryAdmin)
+ who_filter = WhoFilter(
+ request=None, params={}, model=Entry, model_admin=EntryAdmin
+ )
mock_request = MagicMock()
mock_model_admin = MagicMock(spec=EntryAdmin)
@@ -35,7 +41,9 @@ def test_who_filter_lookups(self):
def test_who_filter_queryset_with_value(self):
"""Test that the queryset is filtered correctly when a user is selected."""
- who_filter = WhoFilter(request=None, params={"who": ["admin"]}, model=Entry, model_admin=EntryAdmin)
+ who_filter = WhoFilter(
+ request=None, params={"who": ["admin"]}, model=Entry, model_admin=EntryAdmin
+ )
queryset = Entry.objects.all()
filtered_queryset = who_filter.queryset(None, queryset)
diff --git a/scram/route_manager/tests/test_api.py b/django/src/scram/route_manager/tests/test_api.py
similarity index 96%
rename from scram/route_manager/tests/test_api.py
rename to django/src/scram/route_manager/tests/test_api.py
index 047d5f4b..64a21d18 100644
--- a/scram/route_manager/tests/test_api.py
+++ b/django/src/scram/route_manager/tests/test_api.py
@@ -14,7 +14,9 @@ class TestAddRemoveIP(APITestCase):
def setUp(self):
"""Set up the environment for our tests."""
self.url = reverse("api:v1:entry-list")
- self.superuser = get_user_model().objects.create_superuser("admin", "admin@es.net", "admintestpassword")
+ self.superuser = get_user_model().objects.create_superuser(
+ "admin", "admin@es.net", "admintestpassword"
+ )
self.client.login(username="admin", password="admintestpassword")
self.authorized_client = Client.objects.create(
client_name="authorized_client.es.net",
@@ -117,7 +119,9 @@ def test_unauthenticated_users_have_no_create_access(self):
def test_unauthenticated_users_have_no_ignore_create_access(self):
"""Ensure an unauthenticated client can't add an IgnoreEntry."""
- response = self.client.post(self.ignore_url, {"route": "192.0.2.4"}, format="json")
+ response = self.client.post(
+ self.ignore_url, {"route": "192.0.2.4"}, format="json"
+ )
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_unauthenticated_users_have_no_list_access(self):
@@ -138,7 +142,9 @@ def setUp(self):
is_authorized=True,
)
self.authorized_client.authorized_actiontypes.set([1])
- self.actiontype, _ = ActionType.objects.get_or_create(pk=1, defaults={"name": "block"})
+ self.actiontype, _ = ActionType.objects.get_or_create(
+ pk=1, defaults={"name": "block"}
+ )
# Create some active entries
diff --git a/scram/route_manager/tests/test_authorization.py b/django/src/scram/route_manager/tests/test_authorization.py
similarity index 96%
rename from scram/route_manager/tests/test_authorization.py
rename to django/src/scram/route_manager/tests/test_authorization.py
index 9f00dfbd..a8925c8d 100644
--- a/scram/route_manager/tests/test_authorization.py
+++ b/django/src/scram/route_manager/tests/test_authorization.py
@@ -29,7 +29,9 @@ def setUp(self):
self.readwrite_user.groups.set([self.readwrite_group])
self.readwrite_user.save()
- self.admin_user = User.objects.create(username="admin", is_staff=True, is_superuser=True)
+ self.admin_user = User.objects.create(
+ username="admin", is_staff=True, is_superuser=True
+ )
self.write_blocked_users = [None, self.unauthorized_user, self.readonly_user]
self.write_allowed_users = [self.readwrite_user, self.admin_user]
@@ -104,7 +106,9 @@ def test_unauthorized_detail_view(self):
for user in self.detail_blocked_users:
if user:
self.client.force_login(user)
- response = self.client.get(reverse("route_manager:detail", kwargs={"pk": pk}))
+ response = self.client.get(
+ reverse("route_manager:detail", kwargs={"pk": pk})
+ )
self.assertIn(response.status_code, [302, 403], msg=f"username={user}")
def test_authorized_detail_view(self):
@@ -113,7 +117,9 @@ def test_authorized_detail_view(self):
for user in self.detail_allowed_users:
self.client.force_login(user)
- response = self.client.get(reverse("route_manager:detail", kwargs={"pk": pk}))
+ response = self.client.get(
+ reverse("route_manager:detail", kwargs={"pk": pk})
+ )
self.assertEqual(response.status_code, 200, msg=f"username={user}")
def test_unauthorized_after_group_removal(self):
diff --git a/scram/route_manager/tests/test_autocreate_admin.py b/django/src/scram/route_manager/tests/test_autocreate_admin.py
similarity index 100%
rename from scram/route_manager/tests/test_autocreate_admin.py
rename to django/src/scram/route_manager/tests/test_autocreate_admin.py
diff --git a/scram/route_manager/tests/test_common/__init__.py b/django/src/scram/route_manager/tests/test_common/__init__.py
similarity index 100%
rename from scram/route_manager/tests/test_common/__init__.py
rename to django/src/scram/route_manager/tests/test_common/__init__.py
diff --git a/scram/route_manager/tests/test_common/steps_common.py b/django/src/scram/route_manager/tests/test_common/steps_common.py
similarity index 93%
rename from scram/route_manager/tests/test_common/steps_common.py
rename to django/src/scram/route_manager/tests/test_common/steps_common.py
index 403767a7..8b966241 100644
--- a/scram/route_manager/tests/test_common/steps_common.py
+++ b/django/src/scram/route_manager/tests/test_common/steps_common.py
@@ -9,7 +9,12 @@
from django import conf
from django.urls import reverse
-from scram.route_manager.models import ActionType, Client, WebSocketMessage, WebSocketSequenceElement
+from scram.route_manager.models import (
+ ActionType,
+ Client,
+ WebSocketMessage,
+ WebSocketSequenceElement,
+)
@given("a {name} actiontype is defined")
@@ -22,9 +27,13 @@ def create_actiontype(context, name):
)
at, _ = ActionType.objects.get_or_create(name=name)
- wsm, _ = WebSocketMessage.objects.get_or_create(msg_type="translator_add", msg_data_route_field="route")
+ wsm, _ = WebSocketMessage.objects.get_or_create(
+ msg_type="translator_add", msg_data_route_field="route"
+ )
wsm.save()
- wsse, _ = WebSocketSequenceElement.objects.get_or_create(websocketmessage=wsm, verb="A", action_type=at)
+ wsse, _ = WebSocketSequenceElement.objects.get_or_create(
+ websocketmessage=wsm, verb="A", action_type=at
+ )
wsse.save()
@@ -167,7 +176,9 @@ def add_ignore_entry(context, value):
@when("we remove the {model} {value}")
def remove_an_object(context, model, value):
"""Remove any model object with the matching value."""
- context.response = context.test.client.delete(reverse(f"api:v1:{model.lower()}-detail", args=[value]))
+ context.response = context.test.client.delete(
+ reverse(f"api:v1:{model.lower()}-detail", args=[value])
+ )
@when("we list the {model}s")
diff --git a/scram/route_manager/tests/test_common/steps_ip.py b/django/src/scram/route_manager/tests/test_common/steps_ip.py
similarity index 90%
rename from scram/route_manager/tests/test_common/steps_ip.py
rename to django/src/scram/route_manager/tests/test_common/steps_ip.py
index e51e0262..2c8c3a8e 100644
--- a/scram/route_manager/tests/test_common/steps_ip.py
+++ b/django/src/scram/route_manager/tests/test_common/steps_ip.py
@@ -27,7 +27,9 @@ def check_route(context, route, model):
def check_ip(context, ip):
"""Find an Entry for the specified IP."""
try:
- context.response = context.test.client.get(reverse("api:v1:entry-detail", args=[ip]))
+ context.response = context.test.client.get(
+ reverse("api:v1:entry-detail", args=[ip])
+ )
context.queryException = None
except ValueError as e:
context.response = None
@@ -57,7 +59,9 @@ def update_entry_comment(context, value, comment):
data = {"comment": comment, "who": context.client.client_name}
context.response = context.test.client.put(
- reverse("api:v1:entry-detail", args=[value]), data=json.dumps(data), content_type="application/json"
+ reverse("api:v1:entry-detail", args=[value]),
+ data=json.dumps(data),
+ content_type="application/json",
)
diff --git a/scram/route_manager/tests/test_history.py b/django/src/scram/route_manager/tests/test_history.py
similarity index 94%
rename from scram/route_manager/tests/test_history.py
rename to django/src/scram/route_manager/tests/test_history.py
index d2848e83..cdb328ac 100644
--- a/scram/route_manager/tests/test_history.py
+++ b/django/src/scram/route_manager/tests/test_history.py
@@ -41,7 +41,9 @@ def test_comments(self):
for r in self.routes:
route_old = Route.objects.get(route=r)
e = Entry.objects.get(route=route_old)
- self.assertEqual(e.get_change_reason(), "Zeek detected a scan from 192.0.2.1.")
+ self.assertEqual(
+ e.get_change_reason(), "Zeek detected a scan from 192.0.2.1."
+ )
route_new = str(route_old).replace("16", "32")
e.route = Route.objects.create(route=route_new)
diff --git a/scram/route_manager/tests/test_pagination.py b/django/src/scram/route_manager/tests/test_pagination.py
similarity index 75%
rename from scram/route_manager/tests/test_pagination.py
rename to django/src/scram/route_manager/tests/test_pagination.py
index dfa0ffaa..38ac3ee0 100644
--- a/scram/route_manager/tests/test_pagination.py
+++ b/django/src/scram/route_manager/tests/test_pagination.py
@@ -21,37 +21,60 @@ def setUp(self):
"""Set up the test environment."""
self.fake = Faker()
self.fake.add_provider(internet)
- get_user_model().objects.create_user(username="testuser", password="testpass123")
+ get_user_model().objects.create_user(
+ username="testuser", password="testpass123"
+ )
self.atype1 = ActionType.objects.create(name="Type1", available=True)
self.atype2 = ActionType.objects.create(name="Type2", available=True)
self.atype3 = ActionType.objects.create(name="Type3", available=False)
# Create enough entries to test pagination
- created_routes = Route.objects.bulk_create([
- Route(route=self.fake.unique.ipv4_public()) for x in range(self.TEST_PAGINATION_SIZE + 3)
- ])
- entries_type1 = Entry.objects.bulk_create([
- Entry(route=route, actiontype=self.atype1, is_active=True) for route in created_routes
- ])
+ created_routes = Route.objects.bulk_create(
+ [
+ Route(route=self.fake.unique.ipv4_public())
+ for x in range(self.TEST_PAGINATION_SIZE + 3)
+ ]
+ )
+ entries_type1 = Entry.objects.bulk_create(
+ [
+ Entry(route=route, actiontype=self.atype1, is_active=True)
+ for route in created_routes
+ ]
+ )
# Create a second type of entries to test filtering per actiontype
- created_routes = Route.objects.bulk_create([Route(route=self.fake.unique.ipv4_public()) for x in range(3)])
- entries_type2 = Entry.objects.bulk_create([
- Entry(route=route, actiontype=self.atype2, is_active=True) for route in created_routes
- ])
+ created_routes = Route.objects.bulk_create(
+ [Route(route=self.fake.unique.ipv4_public()) for x in range(3)]
+ )
+ entries_type2 = Entry.objects.bulk_create(
+ [
+ Entry(route=route, actiontype=self.atype2, is_active=True)
+ for route in created_routes
+ ]
+ )
# Create inactive entries to test filtering by available actiontypes
- created_routes = Route.objects.bulk_create([Route(route=self.fake.unique.ipv4_public()) for x in range(3)])
- Entry.objects.bulk_create([
- Entry(route=route, actiontype=self.atype1, is_active=False) for route in created_routes
- ])
+ created_routes = Route.objects.bulk_create(
+ [Route(route=self.fake.unique.ipv4_public()) for x in range(3)]
+ )
+ Entry.objects.bulk_create(
+ [
+ Entry(route=route, actiontype=self.atype1, is_active=False)
+ for route in created_routes
+ ]
+ )
# Create entries for an invalid actiontype to test that
- created_routes = Route.objects.bulk_create([Route(route=self.fake.unique.ipv4_public()) for x in range(3)])
- Entry.objects.bulk_create([
- Entry(route=route, actiontype=self.atype3, is_active=False) for route in created_routes
- ])
+ created_routes = Route.objects.bulk_create(
+ [Route(route=self.fake.unique.ipv4_public()) for x in range(3)]
+ )
+ Entry.objects.bulk_create(
+ [
+ Entry(route=route, actiontype=self.atype3, is_active=False)
+ for route in created_routes
+ ]
+ )
self.entries = {
"type1": entries_type1,
diff --git a/scram/route_manager/tests/test_process_updates.py b/django/src/scram/route_manager/tests/test_process_updates.py
similarity index 90%
rename from scram/route_manager/tests/test_process_updates.py
rename to django/src/scram/route_manager/tests/test_process_updates.py
index 592fa732..4c8a95cf 100644
--- a/scram/route_manager/tests/test_process_updates.py
+++ b/django/src/scram/route_manager/tests/test_process_updates.py
@@ -5,7 +5,13 @@
import pytest
from django.conf import settings
-from scram.route_manager.models import ActionType, Entry, Route, WebSocketMessage, WebSocketSequenceElement
+from scram.route_manager.models import (
+ ActionType,
+ Entry,
+ Route,
+ WebSocketMessage,
+ WebSocketSequenceElement,
+)
from scram.route_manager.views import check_for_orphaned_history, get_entries_to_process
@@ -142,7 +148,9 @@ def test_multiple_entries_from_different_instances(self, actiontype):
def test_reactivated_entry_found(self, actiontype, other_instance):
"""Reactivated entries are found for reprocessing."""
- entry = create_entry(actiontype, "192.0.2.30/32", other_instance, is_active=False)
+ entry = create_entry(
+ actiontype, "192.0.2.30/32", other_instance, is_active=False
+ )
cutoff = datetime.now(UTC)
entry.is_active = True
entry.save()
@@ -155,7 +163,10 @@ def test_reactivated_entry_found(self, actiontype, other_instance):
def test_future_expiration_entry_active(self, actiontype, other_instance):
"""Entries with future expiration are processed as active."""
_ = create_entry(
- actiontype, "192.0.2.40/32", other_instance, expiration=datetime.now(UTC) + timedelta(hours=1)
+ actiontype,
+ "192.0.2.40/32",
+ other_instance,
+ expiration=datetime.now(UTC) + timedelta(hours=1),
)
cutoff = datetime.now(UTC) - timedelta(minutes=2)
@@ -167,7 +178,10 @@ def test_future_expiration_entry_active(self, actiontype, other_instance):
def test_expired_entry_found_as_inactive(self, actiontype, other_instance):
"""Expired entries are found but marked inactive after process_updates expires them."""
entry = create_entry(
- actiontype, "192.0.2.50/32", other_instance, expiration=datetime.now(UTC) - timedelta(hours=1)
+ actiontype,
+ "192.0.2.50/32",
+ other_instance,
+ expiration=datetime.now(UTC) - timedelta(hours=1),
)
cutoff = datetime.now(UTC) - timedelta(minutes=2)
@@ -176,7 +190,9 @@ def test_expired_entry_found_as_inactive(self, actiontype, other_instance):
assert len(result) == 1
assert result[0].id == entry.id
- def test_processes_entries_from_current_instance(self, actiontype, current_instance):
+ def test_processes_entries_from_current_instance(
+ self, actiontype, current_instance
+ ):
"""Verifies that entries from the current instance are processed (PR #193 fix)."""
entry = create_entry(actiontype, "192.0.2.60/32", current_instance)
cutoff = datetime.now(UTC) - timedelta(minutes=2)
@@ -191,7 +207,9 @@ def test_processes_entries_from_current_instance(self, actiontype, current_insta
class TestCheckForOrphanedHistory:
"""Tests for check_for_orphaned_history()."""
- def test_logs_warning_for_orphaned_entries(self, caplog, actiontype, other_instance):
+ def test_logs_warning_for_orphaned_entries(
+ self, caplog, actiontype, other_instance
+ ):
"""Make sure we log a warning when history exists but Entry was deleted from underneath us."""
entry = create_entry(actiontype, "10.1.0.1/32", other_instance)
orphaned_id = entry.id
diff --git a/scram/route_manager/tests/test_swagger.py b/django/src/scram/route_manager/tests/test_swagger.py
similarity index 100%
rename from scram/route_manager/tests/test_swagger.py
rename to django/src/scram/route_manager/tests/test_swagger.py
diff --git a/scram/route_manager/tests/test_views.py b/django/src/scram/route_manager/tests/test_views.py
similarity index 100%
rename from scram/route_manager/tests/test_views.py
rename to django/src/scram/route_manager/tests/test_views.py
diff --git a/scram/route_manager/tests/test_websockets.py b/django/src/scram/route_manager/tests/test_websockets.py
similarity index 91%
rename from scram/route_manager/tests/test_websockets.py
rename to django/src/scram/route_manager/tests/test_websockets.py
index a37a3d84..b7882190 100644
--- a/scram/route_manager/tests/test_websockets.py
+++ b/django/src/scram/route_manager/tests/test_websockets.py
@@ -33,7 +33,8 @@ async def get_communicators(actiontypes, should_match, *args, **kwds):
"""
router = URLRouter(websocket_urlpatterns)
communicators = [
- WebsocketCommunicator(router, f"/ws/route_manager/translator_{actiontype}/") for actiontype in actiontypes
+ WebsocketCommunicator(router, f"/ws/route_manager/translator_{actiontype}/")
+ for actiontype in actiontypes
]
response = zip(communicators, should_match, strict=True)
@@ -56,7 +57,9 @@ def setUp(self):
"""Set up our test environment."""
# TODO: This is copied from test_api; should de-dupe this.
self.url = reverse("api:v1:entry-list")
- self.superuser = get_user_model().objects.create_superuser("admin", "admin@example.net", "admintestpassword")
+ self.superuser = get_user_model().objects.create_superuser(
+ "admin", "admin@example.net", "admintestpassword"
+ )
self.client.force_login(self.superuser)
self.uuid = "0e7e1cbd-7d73-4968-bc4b-ce3265dc2fd3"
@@ -72,7 +75,9 @@ def setUp(self):
)
self.authorized_client.authorized_actiontypes.set([self.actiontype])
- wsm, _ = WebSocketMessage.objects.get_or_create(msg_type="translator_add", msg_data_route_field="route")
+ wsm, _ = WebSocketMessage.objects.get_or_create(
+ msg_type="translator_add", msg_data_route_field="route"
+ )
_, _ = WebSocketSequenceElement.objects.get_or_create(
websocketmessage=wsm,
verb="A",
@@ -109,7 +114,9 @@ async def get_nothings(self, communicator):
async def add_ip(self, ip, mask):
"""Ensure we can add an IP to block."""
- async with get_communicators(self.actiontypes, self.should_match) as communicators:
+ async with get_communicators(
+ self.actiontypes, self.should_match
+ ) as communicators:
await self.api_create_entry(ip)
# A list of that many function calls to verify the response
@@ -174,14 +181,18 @@ class TranslatorSequenceTestCase(TestTranslatorBaseCase):
def local_setup(self):
"""Define the messages we want to send."""
- wsm2 = WebSocketMessage.objects.create(msg_type="translator_add", msg_data_route_field="foo")
+ wsm2 = WebSocketMessage.objects.create(
+ msg_type="translator_add", msg_data_route_field="foo"
+ )
_ = WebSocketSequenceElement.objects.create(
websocketmessage=wsm2,
verb="A",
action_type=self.actiontype,
order_num=20,
)
- wsm3 = WebSocketMessage.objects.create(msg_type="translator_add", msg_data_route_field="bar")
+ wsm3 = WebSocketMessage.objects.create(
+ msg_type="translator_add", msg_data_route_field="bar"
+ )
_ = WebSocketSequenceElement.objects.create(
websocketmessage=wsm3,
verb="A",
@@ -210,7 +221,9 @@ class TranslatorParametersTestCase(TestTranslatorBaseCase):
def local_setup(self):
"""Define the message we want to send."""
- wsm = WebSocketMessage.objects.get(msg_type="translator_add", msg_data_route_field="route")
+ wsm = WebSocketMessage.objects.get(
+ msg_type="translator_add", msg_data_route_field="route"
+ )
wsm.msg_data = {
"asn": 65550,
"community": 100,
diff --git a/scram/route_manager/urls.py b/django/src/scram/route_manager/urls.py
similarity index 100%
rename from scram/route_manager/urls.py
rename to django/src/scram/route_manager/urls.py
diff --git a/scram/route_manager/views.py b/django/src/scram/route_manager/views.py
similarity index 89%
rename from scram/route_manager/views.py
rename to django/src/scram/route_manager/views.py
index 43c64109..7628c47c 100644
--- a/scram/route_manager/views.py
+++ b/django/src/scram/route_manager/views.py
@@ -41,7 +41,9 @@ def home_page(request, prefilter=None):
if User.objects.count() == 0:
password = make_random_password(length=20)
User.objects.create_superuser("admin", "admin@example.com", password)
- authenticated_admin = authenticate(request, username="admin", password=password)
+ authenticated_admin = authenticate(
+ request, username="admin", password=password
+ )
login(request, authenticated_admin)
messages.add_message(
request,
@@ -60,7 +62,9 @@ def home_page(request, prefilter=None):
readwrite = False
context = {"entries": {}, "readwrite": readwrite}
for at in ActionType.objects.all():
- queryset_active = prefilter.filter(actiontype=at, is_active=True).order_by("-pk")
+ queryset_active = prefilter.filter(actiontype=at, is_active=True).order_by(
+ "-pk"
+ )
context["entries"][at] = {
"objs": queryset_active[:num_entries],
"active": queryset_active.count(),
@@ -85,7 +89,9 @@ def search_entries(request):
str_addr = str(request.POST.get("cidr")).strip()
addr = ipaddress.ip_network(str_addr, strict=False)
except ValueError:
- messages.add_message(request, messages.ERROR, "Search query was not a valid CIDR address")
+ messages.add_message(
+ request, messages.ERROR, "Search query was not a valid CIDR address"
+ )
# Send a 400, but show the home page instead of an error page
return HttpResponseBadRequest(render(request, "route_manager/home.html"))
@@ -142,7 +148,9 @@ def add_entry(request):
elif res.status_code == 403: # noqa: PLR2004
messages.add_message(request, messages.ERROR, "Permission Denied")
else:
- messages.add_message(request, messages.WARNING, f"Something went wrong: {res.status_code}")
+ messages.add_message(
+ request, messages.WARNING, f"Something went wrong: {res.status_code}"
+ )
return redirect("route_manager:home")
@@ -161,7 +169,9 @@ def get_entries_to_process(cutoff_time: timedelta) -> list[Entry]:
logger.debug("Looking for entries modified by any SCRAM instance")
# Grab (only, via values_list) the Entry IDs that have had their history records touched since the cutoff time.
- recently_touched_ids = set(Entry.history.filter(history_date__gt=cutoff_time).values_list("id", flat=True))
+ recently_touched_ids = set(
+ Entry.history.filter(history_date__gt=cutoff_time).values_list("id", flat=True)
+ )
if not recently_touched_ids:
logger.debug("No recently modified entries found")
@@ -170,7 +180,11 @@ def get_entries_to_process(cutoff_time: timedelta) -> list[Entry]:
logger.debug("Found recently touched entry IDs: %s", recently_touched_ids)
# Using the ID's from above, fetch all matching entries and associated models.
- entries_to_process = list(Entry.objects.filter(id__in=recently_touched_ids).select_related("actiontype", "route"))
+ entries_to_process = list(
+ Entry.objects.filter(id__in=recently_touched_ids).select_related(
+ "actiontype", "route"
+ )
+ )
check_for_orphaned_history(recently_touched_ids, entries_to_process)
@@ -178,7 +192,9 @@ def get_entries_to_process(cutoff_time: timedelta) -> list[Entry]:
return entries_to_process
-def check_for_orphaned_history(recently_touched_ids: set[int], entries_to_process: list[Entry]) -> None:
+def check_for_orphaned_history(
+ recently_touched_ids: set[int], entries_to_process: list[Entry]
+) -> None:
"""Check for orphaned history records where the Entry was deleted but history remains.
This shouldn't happen in production since Entry.delete() is overridden on the model,
@@ -193,7 +209,9 @@ def check_for_orphaned_history(recently_touched_ids: set[int], entries_to_proces
# IDs with history but no corresponding Entry row = orphaned (hard-deleted outside of Entry.delete())
orphaned_ids = recently_touched_ids - found_ids
if orphaned_ids:
- logger.warning("Found history records with no corresponding Entry: %s", orphaned_ids)
+ logger.warning(
+ "Found history records with no corresponding Entry: %s", orphaned_ids
+ )
def reprocess_entries(entries_to_process: list[Entry]) -> None:
@@ -215,8 +233,7 @@ def reprocess_entries(entries_to_process: list[Entry]) -> None:
translator_group = f"translator_{entry.actiontype}"
elements = (
- WebSocketSequenceElement.objects
- .filter(action_type__name=entry.actiontype)
+ WebSocketSequenceElement.objects.filter(action_type__name=entry.actiontype)
.order_by("order_num")
.select_related("websocketmessage")
)
@@ -258,12 +275,14 @@ def process_updates(request):
else:
logger.info("No new entries to process")
- return JsonResponse({
- "entries_deleted": entries_start - entries_end,
- "active_entries": entries_end,
- "entries_reprocessed": len(entries_to_process),
- "entries_reprocessed_list": entries_reprocessed_list,
- })
+ return JsonResponse(
+ {
+ "entries_deleted": entries_start - entries_end,
+ "active_entries": entries_end,
+ "entries_reprocessed": len(entries_to_process),
+ "entries_reprocessed_list": entries_reprocessed_list,
+ }
+ )
@require_GET
@@ -293,7 +312,9 @@ def get_context_data(self, **kwargs):
# Get all available action types
for at in ActionType.objects.filter(available=True):
- queryset = Entry.objects.filter(actiontype=at, is_active=True).order_by("-pk")
+ queryset = Entry.objects.filter(actiontype=at, is_active=True).order_by(
+ "-pk"
+ )
# Create a paginator for this action type
paginator = Paginator(queryset, settings.PAGINATION_SIZE)
diff --git a/scram/shared/__init__.py b/django/src/scram/shared/__init__.py
similarity index 100%
rename from scram/shared/__init__.py
rename to django/src/scram/shared/__init__.py
diff --git a/scram/shared/shared_code.py b/django/src/scram/shared/shared_code.py
similarity index 96%
rename from scram/shared/shared_code.py
rename to django/src/scram/shared/shared_code.py
index 926d7bb7..ddd9dba3 100644
--- a/scram/shared/shared_code.py
+++ b/django/src/scram/shared/shared_code.py
@@ -4,7 +4,9 @@
import string
-def make_random_password(length: int = 20, min_digits: int = 5, max_attempts: int = 10000) -> str:
+def make_random_password(
+ length: int = 20, min_digits: int = 5, max_attempts: int = 10000
+) -> str:
"""make_random_password replaces the deprecated django make_random_password function.
Generates a random password of a specified length containing at least a specified number of digits using the
diff --git a/scram/static/css/project.css b/django/src/scram/static/css/project.css
similarity index 100%
rename from scram/static/css/project.css
rename to django/src/scram/static/css/project.css
diff --git a/scram/static/fonts/.gitkeep b/django/src/scram/static/fonts/.gitkeep
similarity index 100%
rename from scram/static/fonts/.gitkeep
rename to django/src/scram/static/fonts/.gitkeep
diff --git a/scram/static/images/favicons/favicon.ico b/django/src/scram/static/images/favicons/favicon.ico
similarity index 100%
rename from scram/static/images/favicons/favicon.ico
rename to django/src/scram/static/images/favicons/favicon.ico
diff --git a/scram/static/js/project.js b/django/src/scram/static/js/project.js
similarity index 100%
rename from scram/static/js/project.js
rename to django/src/scram/static/js/project.js
diff --git a/scram/static/sass/custom_bootstrap_vars.scss b/django/src/scram/static/sass/custom_bootstrap_vars.scss
similarity index 100%
rename from scram/static/sass/custom_bootstrap_vars.scss
rename to django/src/scram/static/sass/custom_bootstrap_vars.scss
diff --git a/scram/static/sass/project.scss b/django/src/scram/static/sass/project.scss
similarity index 100%
rename from scram/static/sass/project.scss
rename to django/src/scram/static/sass/project.scss
diff --git a/scram/templates/403.html b/django/src/scram/templates/403.html
similarity index 100%
rename from scram/templates/403.html
rename to django/src/scram/templates/403.html
diff --git a/scram/templates/404.html b/django/src/scram/templates/404.html
similarity index 100%
rename from scram/templates/404.html
rename to django/src/scram/templates/404.html
diff --git a/scram/templates/500.html b/django/src/scram/templates/500.html
similarity index 100%
rename from scram/templates/500.html
rename to django/src/scram/templates/500.html
diff --git a/scram/templates/base.html b/django/src/scram/templates/base.html
similarity index 100%
rename from scram/templates/base.html
rename to django/src/scram/templates/base.html
diff --git a/scram/templates/local_auth/login.html b/django/src/scram/templates/local_auth/login.html
similarity index 100%
rename from scram/templates/local_auth/login.html
rename to django/src/scram/templates/local_auth/login.html
diff --git a/scram/templates/navbar.html b/django/src/scram/templates/navbar.html
similarity index 100%
rename from scram/templates/navbar.html
rename to django/src/scram/templates/navbar.html
diff --git a/scram/templates/route_manager/entry_detail.html b/django/src/scram/templates/route_manager/entry_detail.html
similarity index 100%
rename from scram/templates/route_manager/entry_detail.html
rename to django/src/scram/templates/route_manager/entry_detail.html
diff --git a/scram/templates/route_manager/entry_list.html b/django/src/scram/templates/route_manager/entry_list.html
similarity index 100%
rename from scram/templates/route_manager/entry_list.html
rename to django/src/scram/templates/route_manager/entry_list.html
diff --git a/scram/templates/route_manager/home.html b/django/src/scram/templates/route_manager/home.html
similarity index 100%
rename from scram/templates/route_manager/home.html
rename to django/src/scram/templates/route_manager/home.html
diff --git a/scram/templates/users/user_detail.html b/django/src/scram/templates/users/user_detail.html
similarity index 100%
rename from scram/templates/users/user_detail.html
rename to django/src/scram/templates/users/user_detail.html
diff --git a/scram/templates/users/user_form.html b/django/src/scram/templates/users/user_form.html
similarity index 100%
rename from scram/templates/users/user_form.html
rename to django/src/scram/templates/users/user_form.html
diff --git a/scram/users/__init__.py b/django/src/scram/users/__init__.py
similarity index 100%
rename from scram/users/__init__.py
rename to django/src/scram/users/__init__.py
diff --git a/scram/users/admin.py b/django/src/scram/users/admin.py
similarity index 100%
rename from scram/users/admin.py
rename to django/src/scram/users/admin.py
diff --git a/scram/users/api/serializers.py b/django/src/scram/users/api/serializers.py
similarity index 77%
rename from scram/users/api/serializers.py
rename to django/src/scram/users/api/serializers.py
index f8d22c88..3020193a 100644
--- a/scram/users/api/serializers.py
+++ b/django/src/scram/users/api/serializers.py
@@ -15,4 +15,6 @@ class Meta:
model = User
fields = ["username", "name", "url"]
- extra_kwargs = {"url": {"view_name": "api:v1:user-detail", "lookup_field": "username"}}
+ extra_kwargs = {
+ "url": {"view_name": "api:v1:user-detail", "lookup_field": "username"}
+ }
diff --git a/scram/users/api/views.py b/django/src/scram/users/api/views.py
similarity index 100%
rename from scram/users/api/views.py
rename to django/src/scram/users/api/views.py
diff --git a/scram/users/apps.py b/django/src/scram/users/apps.py
similarity index 100%
rename from scram/users/apps.py
rename to django/src/scram/users/apps.py
diff --git a/scram/users/forms.py b/django/src/scram/users/forms.py
similarity index 85%
rename from scram/users/forms.py
rename to django/src/scram/users/forms.py
index 3807fdac..0a74e252 100644
--- a/scram/users/forms.py
+++ b/django/src/scram/users/forms.py
@@ -24,4 +24,6 @@ class Meta(admin_forms.AdminUserCreationForm.Meta):
model = User
- error_messages = {"username": {"unique": _("This username has already been taken.")}}
+ error_messages = {
+ "username": {"unique": _("This username has already been taken.")}
+ }
diff --git a/scram/users/migrations/0001_initial.py b/django/src/scram/users/migrations/0001_initial.py
similarity index 100%
rename from scram/users/migrations/0001_initial.py
rename to django/src/scram/users/migrations/0001_initial.py
diff --git a/scram/users/migrations/__init__.py b/django/src/scram/users/migrations/__init__.py
similarity index 100%
rename from scram/users/migrations/__init__.py
rename to django/src/scram/users/migrations/__init__.py
diff --git a/scram/users/models.py b/django/src/scram/users/models.py
similarity index 100%
rename from scram/users/models.py
rename to django/src/scram/users/models.py
diff --git a/scram/users/tests/__init__.py b/django/src/scram/users/tests/__init__.py
similarity index 100%
rename from scram/users/tests/__init__.py
rename to django/src/scram/users/tests/__init__.py
diff --git a/scram/users/tests/factories.py b/django/src/scram/users/tests/factories.py
similarity index 100%
rename from scram/users/tests/factories.py
rename to django/src/scram/users/tests/factories.py
diff --git a/scram/users/tests/test_admin.py b/django/src/scram/users/tests/test_admin.py
similarity index 100%
rename from scram/users/tests/test_admin.py
rename to django/src/scram/users/tests/test_admin.py
diff --git a/scram/users/tests/test_drf_urls.py b/django/src/scram/users/tests/test_drf_urls.py
similarity index 84%
rename from scram/users/tests/test_drf_urls.py
rename to django/src/scram/users/tests/test_drf_urls.py
index 8e587486..f4fe9375 100644
--- a/scram/users/tests/test_drf_urls.py
+++ b/django/src/scram/users/tests/test_drf_urls.py
@@ -10,7 +10,10 @@
def test_user_detail(user: User):
"""Ensure we can view details for a single User."""
- assert reverse("api:v1:user-detail", kwargs={"username": user.username}) == f"/api/v1/users/{user.username}/"
+ assert (
+ reverse("api:v1:user-detail", kwargs={"username": user.username})
+ == f"/api/v1/users/{user.username}/"
+ )
assert resolve(f"/api/v1/users/{user.username}/").view_name == "api:v1:user-detail"
diff --git a/scram/users/tests/test_drf_views.py b/django/src/scram/users/tests/test_drf_views.py
similarity index 100%
rename from scram/users/tests/test_drf_views.py
rename to django/src/scram/users/tests/test_drf_views.py
diff --git a/scram/users/tests/test_forms.py b/django/src/scram/users/tests/test_forms.py
similarity index 100%
rename from scram/users/tests/test_forms.py
rename to django/src/scram/users/tests/test_forms.py
diff --git a/scram/users/tests/test_models.py b/django/src/scram/users/tests/test_models.py
similarity index 100%
rename from scram/users/tests/test_models.py
rename to django/src/scram/users/tests/test_models.py
diff --git a/scram/users/tests/test_urls.py b/django/src/scram/users/tests/test_urls.py
similarity index 86%
rename from scram/users/tests/test_urls.py
rename to django/src/scram/users/tests/test_urls.py
index aa5a57ad..15ecb1cd 100644
--- a/scram/users/tests/test_urls.py
+++ b/django/src/scram/users/tests/test_urls.py
@@ -10,7 +10,10 @@
def test_detail(user: User):
"""Ensure we can get the URL to view details about a single User."""
- assert reverse("users:detail", kwargs={"username": user.username}) == f"/users/{user.username}/"
+ assert (
+ reverse("users:detail", kwargs={"username": user.username})
+ == f"/users/{user.username}/"
+ )
assert resolve(f"/users/{user.username}/").view_name == "users:detail"
diff --git a/scram/users/tests/test_views.py b/django/src/scram/users/tests/test_views.py
similarity index 100%
rename from scram/users/tests/test_views.py
rename to django/src/scram/users/tests/test_views.py
diff --git a/scram/users/urls.py b/django/src/scram/users/urls.py
similarity index 100%
rename from scram/users/urls.py
rename to django/src/scram/users/urls.py
diff --git a/scram/users/views.py b/django/src/scram/users/views.py
similarity index 100%
rename from scram/users/views.py
rename to django/src/scram/users/views.py
diff --git a/scram/utils/__init__.py b/django/src/scram/utils/__init__.py
similarity index 100%
rename from scram/utils/__init__.py
rename to django/src/scram/utils/__init__.py
diff --git a/scram/utils/context_processors.py b/django/src/scram/utils/context_processors.py
similarity index 100%
rename from scram/utils/context_processors.py
rename to django/src/scram/utils/context_processors.py
diff --git a/pyproject.toml b/pyproject.toml
index d593bebb..b454de01 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,13 +1,13 @@
[project]
name = "SCRAM"
-version = "1.5.1"
requires-python = ">=3.12"
+version = "1.5.1"
# ==== pytest ====
[tool.pytest.ini_options]
addopts = [
"--ds=config.settings.test",
- "--ignore=scheduler"
+ "--ignore=scheduler",
]
minversion = "6.0"
python_files = [
@@ -112,42 +112,14 @@ max-complexity = 7 # our current code adheres to this without too much effort
"**/views.py" = [
"DOC201", # documenting return values; it's fairly obvious in a View
]
-"scram/route_manager/**" = [
- "DOC201", # documenting return values
-]
-"scram/users/**" = [
- "DOC201", # documenting return values
- "FBT001", # minimal issue; don't need to mess with in the User app
- "PLR2004", # magic values when checking HTTP status codes
-]
-"test.py" = [
- "S105", # hardcoded password as argument
-]
[tool.ruff.lint.pydocstyle]
convention = "google"
-# ==== mypy ====
-[tool.mypy]
-check_untyped_defs = true
-ignore_missing_imports = true
-plugins = [
- "mypy_django_plugin.main",
- "mypy_drf_plugin.main",
-]
-python_version = "3.11"
-warn_redundant_casts = true
-warn_unused_configs = true
-warn_unused_ignores = true
-
-[[tool.mypy.overrides]]
-# Django migrations should not produce any errors:
-ignore_errors = true
-module = "*.migrations.*"
-
-[tool.django-stubs]
-django_settings_module = "config.settings.test"
-
-
[tool.uv.workspace]
-members = ["scheduler", "translator"]
+members = ["django", "scheduler", "translator"]
+
+[dependency-groups]
+dev = [
+ "ruff~=0.14.14",
+]
diff --git a/scram/__init__.py b/scram/__init__.py
deleted file mode 100644
index 806191d9..00000000
--- a/scram/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""The Django project for Security Catch and Release Automation Manager (SCRAM)."""
-
-__version__ = "1.5.1"
-__version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")) # noqa: RUF067
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 2c082581..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,29 +0,0 @@
-[flake8]
-ignore = F811, W503
-max-line-length = 120
-exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
-
-[pycodestyle]
-max-line-length = 120
-exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
-
-[mypy]
-python_version = 3.8
-check_untyped_defs = True
-ignore_missing_imports = True
-warn_unused_ignores = True
-warn_redundant_casts = True
-warn_unused_configs = True
-plugins = mypy_django_plugin.main
-exclude = scram/route_manager/tests/*
-
-[mypy.plugins.django-stubs]
-django_settings_module = config.settings.test
-
-[mypy-*.migrations.*]
-# Django migrations should not produce any errors:
-ignore_errors = True
-
-[behave]
-paths = scram/route_manager/tests/acceptance
-stderr_capture = no
diff --git a/translator/src/translator/shared.py b/translator/src/translator/shared.py
index 6a21b02d..0f6ccd1a 100644
--- a/translator/src/translator/shared.py
+++ b/translator/src/translator/shared.py
@@ -11,12 +11,12 @@ def asn_is_valid(asn: int) -> bool:
Args:
asn (int): The Autonomous System Number that we want to validate
- Raises:
- ASNError: If the ASN is not between 0 and 4294967295 or is not an integer.
-
Returns:
bool: _description_
+ Raises:
+ ASNError: If the ASN is not between 0 and 4294967295 or is not an integer.
+
"""
if not isinstance(asn, int):
msg = f"ASN {asn} is not an Integer, has type {type(asn)}"
diff --git a/uv.lock b/uv.lock
index 86d5dca9..cc4b2ba3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -6,6 +6,7 @@ requires-python = ">=3.12.9"
members = [
"scheduler",
"scram",
+ "scram-django",
"translator",
]
@@ -1025,6 +1026,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/98/1dd1a5c060916cf21d15e67b7d6a7078e26e2605d5c37cbc9f4f5454c478/redis-7.2.1-py3-none-any.whl", hash = "sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33", size = 396057, upload-time = "2026-02-25T20:05:16.533Z" },
]
+[[package]]
+name = "ruff"
+version = "0.14.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
+ { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
+ { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
+ { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
+]
+
[[package]]
name = "scheduler"
version = "0.1.0"
@@ -1057,6 +1084,21 @@ name = "scram"
version = "1.5.1"
source = { virtual = "." }
+[package.dev-dependencies]
+dev = [
+ { name = "ruff" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [{ name = "ruff", specifier = "~=0.14.14" }]
+
+[[package]]
+name = "scram-django"
+version = "1.5.1"
+source = { editable = "django" }
+
[[package]]
name = "setuptools"
version = "82.0.0"