refactor(packaging): adopt src/ layout with split server/viewer packages

Move all Python source under src/ following modern packaging conventions.
Server, viewer, host-agent, and shared common code now live as four
top-level packages with clear excision boundaries — anthias_viewer can
be removed wholesale when the rewrite-out-of-Python lands without
touching the server.

  src/anthias_common/         shared: errors, utils, internal_auth, device_helper
  src/anthias_server/         Django app, REST API, Celery tasks, manage.py
    lib/                      server-only: auth, backup_helper, diagnostics, github, telemetry
  src/anthias_viewer/         player runtime (was viewer/)
  src/anthias_host_agent/     systemd-driven host shim (was host_agent.py)
  tools/raspberry_pi_imager/  moved from repo root
  tests/conftest.py           moved from repo root

pyproject.toml gets [build-system], setuptools src/ discovery, and an
anthias-manage console script. Django AppConfigs keep label='anthias_app'
and label='api' so existing migration dependency tuples don't move.
BASE_DIR computed from parents[3] to keep templates/static at repo root.
mypy_path set to ["src", "stubs"] with explicit_package_bases.

Dockerfile templates set PYTHONPATH=/usr/src/app/src; bin/start_*.sh
and CI workflows use python -m anthias_server.manage / python -m
anthias_viewer instead of bare ./manage.py and python -m viewer.
Ansible host-agent unit invokes python -m anthias_host_agent.

Verified end-to-end in the docker test container:
  - 430 unit tests pass (matches baseline)
  - 7 integration tests pass, 5 skipped (matches baseline)
  - ruff, mypy clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Viktor Petersson
2026-05-03 06:35:19 +00:00
parent 0a826457ca
commit 8dbf4eabdb
123 changed files with 479 additions and 453 deletions

View File

@@ -7,7 +7,7 @@ on:
paths:
- '.github/workflows/deploy-website.yaml'
- 'website/**'
- 'raspberry_pi_imager/**'
- 'tools/raspberry_pi_imager/**'
# Re-deploy the marketing site whenever a release is published — the
# build-balena-disk-image workflow's final step creates the GitHub
@@ -51,7 +51,7 @@ jobs:
version: ${{ env.UV_VERSION }}
- name: Run rpi-imager tests
run: uv run --no-dev --group website -m pytest --confcutdir=raspberry_pi_imager raspberry_pi_imager/tests/ -v
run: uv run --no-dev --group website -m pytest --confcutdir=raspberry_pi_imager tools/raspberry_pi_imager/tests/ -v
# Build job
build:
@@ -93,7 +93,7 @@ jobs:
run: hugo --source website --minify
- name: Build Raspberry Pi Imager JSON
run: uv run --no-dev --group website python -m raspberry_pi_imager.build_pi_imager_json > website/public/rpi-imager.json
run: uv run --no-dev --group website python -m tools.raspberry_pi_imager.build_pi_imager_json > website/public/rpi-imager.json
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -28,7 +28,7 @@ on:
- '!docker/Dockerfile.dev'
- '!.cursor/**'
- '!.claude/**'
- '!host_agent.py'
- '!src/anthias_host_agent/**'
workflow_dispatch:
jobs:

View File

@@ -97,7 +97,7 @@ jobs:
- name: Generate OpenAPI Schema
run: |
docker compose exec anthias-server \
./manage.py spectacular \
python -m anthias_server.manage spectacular \
--format openapi-json \
--file anthias-api-schema.json

View File

@@ -44,7 +44,7 @@ jobs:
- name: Prepare config directory
run: |
# The django-stubs plugin imports anthias_django.settings, which
# The django-stubs plugin imports anthias_server.django_project.settings, which
# transitively instantiates AnthiasSettings(). On first run that
# tries to read ~/.anthias/anthias.conf — write a minimal valid
# one with the section headers AnthiasSettings expects.

View File

@@ -81,7 +81,7 @@ jobs:
docker compose exec anthias-test \
cp .coverage .coverage.unit-snapshot
# ANTHIAS_INTEGRATION_TEST=1 tells anthias_django/settings.py to
# ANTHIAS_INTEGRATION_TEST=1 tells src/anthias_server/django_project/settings.py to
# pin DATABASES.default.TEST.NAME to the same SQLite path the
# anthias-server container reads. Selenium tests assert on
# `Asset.objects.all()` after the server persists an upload, so

View File

@@ -10,7 +10,8 @@ StartLimitBurst=3
Type=simple
User={{ anthias_user }}
WorkingDirectory=/home/{{ anthias_user }}/anthias
ExecStart=/home/{{ anthias_user }}/installer_venv/bin/python /home/{{ anthias_user }}/anthias/host_agent.py
ExecStart=/home/{{ anthias_user }}/installer_venv/bin/python -m anthias_host_agent
Environment=PYTHONPATH=/home/{{ anthias_user }}/anthias/src
Restart=on-failure
RestartSec=10s
@@ -19,7 +20,7 @@ StandardOutput=journal
StandardError=journal
SyslogIdentifier=anthias-host-agent
# Sandboxing — host_agent.py shells out to `sudo systemctl reboot|poweroff`,
# Sandboxing — anthias_host_agent shells out to `sudo systemctl reboot|poweroff`,
# so this profile must NOT set NoNewPrivileges. On systemd <= 252 (Debian
# 11/12), most Protect*/Restrict* directives implicitly enable NNP and
# would block sudo's setuid escalation, so we keep only the directives

View File

@@ -1,6 +1,6 @@
{{ anthias_user }} ALL=NOPASSWD: /usr/local/sbin/upgrade_anthias.sh
# Restricted to the specific systemctl verbs host_agent.py invokes.
# Path must match what host_agent.py actually calls (/usr/bin/systemctl).
# Restricted to the specific systemctl verbs anthias_host_agent invokes.
# Path must match what anthias_host_agent actually calls (/usr/bin/systemctl).
# Unrestricted systemctl would let the user start/stop/mask any unit.
{{ anthias_user }} ALL=NOPASSWD: /usr/bin/systemctl reboot, /usr/bin/systemctl poweroff

View File

@@ -1,13 +0,0 @@
from api.urls.v1 import get_url_patterns as get_url_patterns_v1
from api.urls.v1_1 import get_url_patterns as get_url_patterns_v1_1
from api.urls.v1_2 import get_url_patterns as get_url_patterns_v1_2
from api.urls.v2 import get_url_patterns as get_url_patterns_v2
app_name = 'api'
urlpatterns = [
*get_url_patterns_v1(),
*get_url_patterns_v1_1(),
*get_url_patterns_v1_2(),
*get_url_patterns_v2(),
]

View File

@@ -50,9 +50,9 @@ if [ "$START_SERVER" = true ]; then
bun install && bun run build
./manage.py makemigrations
./manage.py migrate --fake-initial
uvicorn anthias_django.asgi:application \
python -m anthias_server.manage makemigrations
python -m anthias_server.manage migrate --fake-initial
uvicorn anthias_server.django_project.asgi:application \
--host 127.0.0.1 --port 8080 &
sleep 3

View File

@@ -22,12 +22,12 @@ echo "Running migration..."
# database is not left in an inconsistent state if the migration fails.
if [ -f /data/.anthias/anthias.db ]; then
./manage.py dbbackup --noinput --clean && \
./manage.py migrate --fake-initial --noinput || \
./manage.py dbrestore --noinput
python -m anthias_server.manage dbbackup --noinput --clean && \
python -m anthias_server.manage migrate --fake-initial --noinput || \
python -m anthias_server.manage dbrestore --noinput
else
./manage.py migrate && \
./manage.py dbbackup --noinput --clean
python -m anthias_server.manage migrate && \
python -m anthias_server.manage dbbackup --noinput --clean
fi
UVICORN_BIND_HOST="${LISTEN:-0.0.0.0}"
@@ -51,7 +51,7 @@ if [[ "$ENVIRONMENT" == "development" ]]; then
echo "Building frontend assets..."
bun install && bun run build
echo "Starting uvicorn (development, --reload)..."
exec uvicorn anthias_django.asgi:application \
exec uvicorn anthias_server.django_project.asgi:application \
--host "$UVICORN_BIND_HOST" \
--port "$UVICORN_BIND_PORT" \
--timeout-keep-alive 30 \
@@ -60,9 +60,9 @@ if [[ "$ENVIRONMENT" == "development" ]]; then
"${UVICORN_PROXY_ARGS[@]}"
else
echo "Generating Django static files..."
./manage.py collectstatic --clear --noinput
python -m anthias_server.manage collectstatic --clear --noinput
echo "Starting uvicorn..."
exec uvicorn anthias_django.asgi:application \
exec uvicorn anthias_server.django_project.asgi:application \
--host "$UVICORN_BIND_HOST" \
--port "$UVICORN_BIND_PORT" \
--timeout-keep-alive 30 \

View File

@@ -100,8 +100,8 @@ fi
# --preserve-env=XDG_RUNTIME_DIR forces sudo to forward the runtime dir
# we just set; -E alone is subject to env_check / env_delete and is not
# guaranteed for XDG_* on Debian's default sudoers.
sudo --preserve-env=XDG_RUNTIME_DIR,QT_SCALE_FACTOR -E -u viewer \
dbus-run-session /venv/bin/python -m viewer &
sudo --preserve-env=XDG_RUNTIME_DIR,QT_SCALE_FACTOR,PYTHONPATH -E -u viewer \
dbus-run-session /venv/bin/python -m anthias_viewer &
# Wait for the viewer
while true; do

View File

@@ -49,7 +49,7 @@ services:
# See docker-compose.yml.tmpl for context on the merge.
image: ghcr.io/screenly/anthias-server:${GIT_SHORT_HASH}-${BOARD}
command: >
celery -A celery_tasks.celery worker -B -n worker@anthias
celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias
--loglevel=info --schedule /tmp/celerybeat-schedule
depends_on:
- anthias-server

View File

@@ -43,7 +43,7 @@ services:
# See docker-compose.yml.tmpl for context on the merge.
image: ghcr.io/screenly/anthias-server:${GIT_SHORT_HASH}-${BOARD}
command: >
celery -A celery_tasks.celery worker -B -n worker@anthias
celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias
--loglevel=info --schedule /tmp/celerybeat-schedule
depends_on:
- anthias-server

View File

@@ -35,7 +35,7 @@ services:
redis:
condition: service_started
command: >
celery -A celery_tasks.celery worker -B -n worker@anthias
celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias
--loglevel=info --schedule /tmp/celerybeat-schedule
environment:
- HOME=/data

View File

@@ -22,7 +22,7 @@ services:
# Pin the test DB to the writable volume mount on /data so CI
# keeps its historic location. Local runs (no override) fall back
# to a repo-local path inside BASE_DIR — see
# anthias_django/settings.py.
# src/anthias_server/django_project/settings.py.
- ANTHIAS_TEST_DB_PATH=/data/.anthias/test.db
stdin_open: true
tty: true
@@ -39,7 +39,7 @@ services:
image: anthias-test:dev
build: *test_build
command: >
celery -A celery_tasks.celery worker -B -n worker@anthias
celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias
--loglevel=info --schedule /tmp/celerybeat-schedule
depends_on:
anthias-test:

View File

@@ -65,7 +65,7 @@ services:
# identical content per device. See refactor: drop celery image.
image: ghcr.io/screenly/anthias-server:${DOCKER_TAG}-${DEVICE_TYPE}
command: >
celery -A celery_tasks.celery worker -B -n worker@anthias
celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias
--loglevel=info --schedule /tmp/celerybeat-schedule
depends_on:
- anthias-server

View File

@@ -60,6 +60,7 @@ ENV GIT_HASH={{ git_hash }}
ENV GIT_SHORT_HASH={{ git_short_hash }}
ENV GIT_BRANCH={{ git_branch }}
ENV DEVICE_TYPE={{ device_type }}
ENV DJANGO_SETTINGS_MODULE="anthias_django.settings"
ENV DJANGO_SETTINGS_MODULE="anthias_server.django_project.settings"
ENV PYTHONPATH="/usr/src/app/src"
CMD ["bash", "bin/start_server.sh"]

View File

@@ -63,5 +63,6 @@ RUN cp ansible/roles/anthias/files/anthias.conf \
ENV GIT_HASH={{ git_hash }}
ENV GIT_SHORT_HASH={{ git_short_hash }}
ENV GIT_BRANCH={{ git_branch }}
ENV DJANGO_SETTINGS_MODULE="anthias_django.settings"
ENV DJANGO_SETTINGS_MODULE="anthias_server.django_project.settings"
ENV PYTHONPATH="/usr/src/app/src"
ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:$PATH"

View File

@@ -63,7 +63,8 @@ ENV GIT_HASH={{ git_hash }}
ENV GIT_SHORT_HASH={{ git_short_hash }}
ENV GIT_BRANCH={{ git_branch }}
ENV DEVICE_TYPE={{ device_type }}
ENV DJANGO_SETTINGS_MODULE="anthias_django.settings"
ENV DJANGO_SETTINGS_MODULE="anthias_server.django_project.settings"
ENV PYTHONPATH="/usr/src/app/src"
ENV ANTHIAS_SERVICE="viewer"
RUN useradd -g video viewer

View File

@@ -1,3 +1,7 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "anthias"
version = "0.20.4"
@@ -6,6 +10,14 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[project.scripts]
anthias-manage = "anthias_server.manage:main"
[tool.setuptools.packages.find]
where = ["src"]
include = ["anthias_*"]
namespaces = false
[dependency-groups]
dev-host = [
"ansible-lint==26.4.0",
@@ -106,7 +118,7 @@ test = [
{ include-group = "dev" },
]
# Used by the python-mypy CI job. The django-stubs plugin imports
# anthias_django.settings to introspect the app registry, so we need the
# anthias_server.django_project.settings to introspect the app registry, so we need the
# runtime deps that settings touches — but not the heavy native-extension
# deps from the server group (cec, netifaces, etc.). We also include
# docker-image-builder so its tools (pygit2, python_on_whales) resolve.
@@ -125,8 +137,8 @@ mypy = [
]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "anthias_django.settings"
testpaths = ["tests", "api/tests"]
DJANGO_SETTINGS_MODULE = "anthias_server.django_project.settings"
testpaths = ["tests", "src/anthias_server/api/tests"]
python_files = ["test_*.py"]
markers = [
"integration: marks tests as integration (deselect with '-m \"not integration\"')",
@@ -136,26 +148,24 @@ addopts = "--strict-markers"
[tool.coverage.run]
branch = true
source = [
"anthias_app",
"api",
"celery_tasks",
"lib",
"settings",
"viewer",
"src/anthias_server",
"src/anthias_common",
"src/anthias_viewer",
]
omit = [
"*/migrations/*",
"*/__init__.py",
"anthias_django/asgi.py",
"anthias_django/routing.py",
"anthias_django/urls.py",
"anthias_django/wsgi.py",
"anthias_app/management/*",
"src/anthias_server/django_project/asgi.py",
"src/anthias_server/django_project/routing.py",
"src/anthias_server/django_project/urls.py",
"src/anthias_server/django_project/wsgi.py",
"src/anthias_server/app/management/*",
"src/anthias_server/manage.py",
"src/anthias_host_agent/*",
"bin/*",
"host_agent.py",
"raspberry_pi_imager/*",
"tools/raspberry_pi_imager/*",
"tools/image_builder/*",
"viewer/__main__.py",
"src/anthias_viewer/__main__.py",
]
[tool.coverage.report]
@@ -176,7 +186,8 @@ output = "coverage.xml"
python_version = "3.13"
strict = true
follow_imports = "normal"
mypy_path = "stubs"
mypy_path = ["src", "stubs"]
explicit_package_bases = true
plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"]
exclude = [
"^\\.venv/",
@@ -204,4 +215,4 @@ module = [
ignore_missing_imports = true
[tool.django-stubs]
django_settings_module = "anthias_django.settings"
django_settings_module = "anthias_server.django_project.settings"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env python3
from raspberry_pi_imager.build_pi_imager_json import main
if __name__ == '__main__':
main()

View File

@@ -1,5 +1,5 @@
line-length = 79
exclude = ["anthias_app/migrations/*.py"]
exclude = ["src/anthias_server/app/migrations/*.py", "src/anthias_server/api/migrations/*.py"]
target-version = "py313"
[format]

View File

@@ -25,8 +25,8 @@ from tenacity import (
wait_fixed,
)
from anthias_app.models import Asset
from settings import settings
from anthias_server.app.models import Asset
from anthias_server.settings import settings
arch = machine()
@@ -495,7 +495,7 @@ class YoutubeDownloadThread(Thread):
return
# Imported lazily so the viewer container (which does not
# ship channels/channels-redis) can still import lib.utils.
# ship channels/channels-redis) can still import anthias_common.utils.
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

View File

@@ -0,0 +1 @@

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
name = 'anthias_server.api'
label = 'api'

View File

@@ -6,7 +6,7 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import exception_handler
from anthias_app.models import Asset
from anthias_server.app.models import Asset
class AssetCreationError(Exception):

View File

@@ -11,8 +11,8 @@ from rest_framework.serializers import (
Serializer,
)
from anthias_app.models import Asset
from lib.utils import validate_url
from anthias_server.app.models import Asset
from anthias_common.utils import validate_url
def get_unique_name(name: str) -> str:

View File

@@ -5,13 +5,13 @@ from typing import Any
from rest_framework.serializers import CharField, Serializer
from api.errors import AssetCreationError
from lib.utils import (
from anthias_server.api.errors import AssetCreationError
from anthias_common.utils import (
download_video_from_youtube,
get_video_duration,
url_fails,
)
from settings import settings
from anthias_server.settings import settings
from . import (
get_unique_name,

View File

@@ -12,12 +12,12 @@ from rest_framework.serializers import (
Serializer,
)
from lib.utils import (
from anthias_common.utils import (
download_video_from_youtube,
get_video_duration,
url_fails,
)
from settings import settings
from anthias_server.settings import settings
from . import (
get_unique_name,

View File

@@ -8,7 +8,7 @@ from rest_framework.serializers import (
Serializer,
)
from api.serializers.mixins import CreateAssetSerializerMixin
from anthias_server.api.serializers.mixins import CreateAssetSerializerMixin
class CreateAssetSerializerV1_2(

View File

@@ -17,9 +17,9 @@ from rest_framework.serializers import (
TimeField,
)
from anthias_app.models import Asset
from api.serializers import UpdateAssetSerializer
from api.serializers.mixins import CreateAssetSerializerMixin
from anthias_server.app.models import Asset
from anthias_server.api.serializers import UpdateAssetSerializer
from anthias_server.api.serializers.mixins import CreateAssetSerializerMixin
def _normalise_play_days(value: list[int]) -> list[int]:

View File

@@ -10,7 +10,7 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from api.tests.test_common import (
from anthias_server.api.tests.test_common import (
ASSET_CREATION_DATA,
ASSET_UPDATE_DATA_V1_2,
ASSET_UPDATE_DATA_V2,
@@ -97,8 +97,8 @@ def test_create_asset_should_return_201(
@pytest.mark.django_db
@mock.patch('api.serializers.mixins.rename')
@mock.patch('api.serializers.mixins.validate_uri')
@mock.patch('anthias_server.api.serializers.mixins.rename')
@mock.patch('anthias_server.api.serializers.mixins.validate_uri')
def test_create_video_asset_v2_with_non_zero_duration_should_fail(
mock_validate_uri: Any, mock_rename: Any, api_client: APIClient
) -> None:

View File

@@ -32,11 +32,11 @@ def _assert_response_data(
@pytest.mark.django_db
@mock.patch('api.views.mixins.is_up_to_date', return_value=False)
@mock.patch('lib.diagnostics.get_load_avg', return_value={'15 min': 0.11})
@mock.patch('api.views.mixins.size', return_value='15G')
@mock.patch('api.views.mixins.statvfs', mock.MagicMock())
@mock.patch('api.views.mixins.r.get', return_value='off')
@mock.patch('anthias_server.api.views.mixins.is_up_to_date', return_value=False)
@mock.patch('anthias_server.lib.diagnostics.get_load_avg', return_value={'15 min': 0.11})
@mock.patch('anthias_server.api.views.mixins.size', return_value='15G')
@mock.patch('anthias_server.api.views.mixins.statvfs', mock.MagicMock())
@mock.patch('anthias_server.api.views.mixins.r.get', return_value='off')
def test_info_v1_endpoint(
redis_get_mock: Any,
size_mock: Any,
@@ -68,22 +68,22 @@ def test_info_v1_endpoint(
@pytest.mark.django_db
@mock.patch('api.views.v2.is_up_to_date', return_value=True)
@mock.patch('lib.diagnostics.get_load_avg', return_value={'15 min': 0.25})
@mock.patch('api.views.v2.size', return_value='20G')
@mock.patch('api.views.v2.statvfs', mock.MagicMock())
@mock.patch('api.views.v2.r.get', return_value='on')
@mock.patch('api.views.v2.diagnostics.get_git_branch', return_value='main')
@mock.patch('anthias_server.api.views.v2.is_up_to_date', return_value=True)
@mock.patch('anthias_server.lib.diagnostics.get_load_avg', return_value={'15 min': 0.25})
@mock.patch('anthias_server.api.views.v2.size', return_value='20G')
@mock.patch('anthias_server.api.views.v2.statvfs', mock.MagicMock())
@mock.patch('anthias_server.api.views.v2.r.get', return_value='on')
@mock.patch('anthias_server.api.views.v2.diagnostics.get_git_branch', return_value='main')
@mock.patch(
'api.views.v2.diagnostics.get_git_short_hash', return_value='a1b2c3d'
'anthias_server.api.views.v2.diagnostics.get_git_short_hash', return_value='a1b2c3d'
)
@mock.patch(
'api.views.v2.device_helper.parse_cpu_info',
'anthias_server.api.views.v2.device_helper.parse_cpu_info',
return_value={'model': 'Raspberry Pi 4'},
)
@mock.patch('api.views.v2.diagnostics.get_uptime', return_value=86400)
@mock.patch('anthias_server.api.views.v2.diagnostics.get_uptime', return_value=86400)
@mock.patch(
'api.views.v2.psutil.virtual_memory',
'anthias_server.api.views.v2.psutil.virtual_memory',
return_value=mock.MagicMock(
total=8192 << 20, # 8GB
used=4096 << 20, # 4GB
@@ -94,10 +94,10 @@ def test_info_v1_endpoint(
),
)
@mock.patch(
'api.views.v2.get_node_mac_address', return_value='00:11:22:33:44:55'
'anthias_server.api.views.v2.get_node_mac_address', return_value='00:11:22:33:44:55'
)
@mock.patch('api.views.v2.get_node_ip', return_value='192.168.1.100 10.0.0.50')
@mock.patch('api.views.v2.getenv', return_value='testuser')
@mock.patch('anthias_server.api.views.v2.get_node_ip', return_value='192.168.1.100 10.0.0.50')
@mock.patch('anthias_server.api.views.v2.getenv', return_value='testuser')
def test_info_v2_endpoint(
getenv_mock: Any,
get_node_ip_mock: Any,

View File

@@ -14,9 +14,9 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from anthias_app.models import Asset
from api.tests.test_common import ASSET_CREATION_DATA
from settings import settings as anthias_settings
from anthias_server.app.models import Asset
from anthias_server.api.tests.test_common import ASSET_CREATION_DATA
from anthias_server.settings import settings as anthias_settings
@pytest.fixture
@@ -112,7 +112,7 @@ def test_playlist_order(
'asset&6ee2394e760643748b9353f06f405424',
],
)
@mock.patch('api.views.v1.ViewerPublisher.send_to_viewer', return_value=None)
@mock.patch('anthias_server.api.views.v1.ViewerPublisher.send_to_viewer', return_value=None)
def test_assets_control(
send_to_viewer_mock: Any,
command: str,
@@ -130,7 +130,7 @@ def test_assets_control(
@pytest.mark.django_db
@mock.patch(
'api.views.mixins.reboot_anthias.apply_async',
'anthias_server.api.views.mixins.reboot_anthias.apply_async',
side_effect=(lambda: None),
)
def test_reboot(
@@ -147,7 +147,7 @@ def test_reboot(
@pytest.mark.django_db
@mock.patch(
'api.views.mixins.shutdown_anthias.apply_async',
'anthias_server.api.views.mixins.shutdown_anthias.apply_async',
side_effect=(lambda: None),
)
def test_shutdown(
@@ -163,7 +163,7 @@ def test_shutdown(
@pytest.mark.django_db
@mock.patch('api.views.v1.ViewerPublisher.send_to_viewer', return_value=None)
@mock.patch('anthias_server.api.views.v1.ViewerPublisher.send_to_viewer', return_value=None)
def test_viewer_current_asset(
send_to_viewer_mock: Any,
api_client: APIClient,
@@ -180,7 +180,7 @@ def test_viewer_current_asset(
recv_json_mock = mock.MagicMock(
return_value={'current_asset_id': asset_id}
)
with mock.patch('api.views.v1.ReplyCollector.recv_json', recv_json_mock):
with mock.patch('anthias_server.api.views.v1.ReplyCollector.recv_json', recv_json_mock):
viewer_current_asset_url = reverse('api:viewer_current_asset_v1')
response = api_client.get(viewer_current_asset_url)
data = response.data

View File

@@ -11,7 +11,7 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from lib.auth import verify_password
from anthias_server.lib.auth import verify_password
@pytest.fixture
@@ -25,7 +25,7 @@ def device_settings_url() -> str:
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.settings')
def test_get_device_settings(
settings_mock: Any, api_client: APIClient, device_settings_url: str
) -> None:
@@ -68,7 +68,7 @@ def test_get_device_settings(
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.settings')
def test_patch_device_settings_invalid_auth_backend(
settings_mock: Any, api_client: APIClient, device_settings_url: str
) -> None:
@@ -103,8 +103,8 @@ def test_patch_device_settings_invalid_auth_backend(
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('api.views.v2.ViewerPublisher')
@mock.patch('anthias_server.api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.ViewerPublisher')
def test_patch_device_settings_success(
publisher_mock: Any,
settings_mock: Any,
@@ -153,7 +153,7 @@ def test_patch_device_settings_success(
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.settings')
def test_patch_device_settings_validation_error(
settings_mock: Any, api_client: APIClient, device_settings_url: str
) -> None:
@@ -173,8 +173,8 @@ def test_patch_device_settings_validation_error(
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('api.views.v2.ViewerPublisher')
@mock.patch('anthias_server.api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.ViewerPublisher')
def test_enable_basic_auth(
publisher_mock: Any,
settings_mock: Any,
@@ -253,8 +253,8 @@ def test_enable_basic_auth(
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('api.views.v2.ViewerPublisher')
@mock.patch('anthias_server.api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.ViewerPublisher')
def test_disable_basic_auth(
publisher_mock: Any,
settings_mock: Any,
@@ -310,10 +310,10 @@ def test_disable_basic_auth(
@pytest.mark.django_db
@mock.patch('api.views.v2.settings')
@mock.patch('api.views.v2.ViewerPublisher')
@mock.patch('api.views.v2.add_default_assets')
@mock.patch('api.views.v2.remove_default_assets')
@mock.patch('anthias_server.api.views.v2.settings')
@mock.patch('anthias_server.api.views.v2.ViewerPublisher')
@mock.patch('anthias_server.api.views.v2.add_default_assets')
@mock.patch('anthias_server.api.views.v2.remove_default_assets')
def test_patch_device_settings_default_assets(
remove_default_assets_mock: Any,
add_default_assets_mock: Any,
@@ -406,8 +406,8 @@ def integrations_url() -> str:
@pytest.mark.django_db
@patch('api.views.v2.is_balena_app')
@patch('api.views.v2.getenv')
@patch('anthias_server.api.views.v2.is_balena_app')
@patch('anthias_server.api.views.v2.getenv')
def test_integrations_balena_environment(
mock_getenv: Any,
mock_is_balena: Any,
@@ -439,7 +439,7 @@ def test_integrations_balena_environment(
@pytest.mark.django_db
@patch('api.views.v2.is_balena_app')
@patch('anthias_server.api.views.v2.is_balena_app')
def test_integrations_non_balena_environment(
mock_is_balena: Any, api_client: APIClient, integrations_url: str
) -> None:

View File

@@ -0,0 +1,13 @@
from anthias_server.api.urls.v1 import get_url_patterns as get_url_patterns_v1
from anthias_server.api.urls.v1_1 import get_url_patterns as get_url_patterns_v1_1
from anthias_server.api.urls.v1_2 import get_url_patterns as get_url_patterns_v1_2
from anthias_server.api.urls.v2 import get_url_patterns as get_url_patterns_v2
app_name = 'api'
urlpatterns = [
*get_url_patterns_v1(),
*get_url_patterns_v1_1(),
*get_url_patterns_v1_2(),
*get_url_patterns_v2(),
]

View File

@@ -1,6 +1,6 @@
from django.urls import URLPattern, URLResolver, path
from api.views.v1 import (
from anthias_server.api.views.v1 import (
AssetContentViewV1,
AssetListViewV1,
AssetsControlViewV1,

View File

@@ -1,6 +1,6 @@
from django.urls import URLPattern, URLResolver, path
from api.views.v1_1 import AssetListViewV1_1, AssetViewV1_1
from anthias_server.api.views.v1_1 import AssetListViewV1_1, AssetViewV1_1
def get_url_patterns() -> list[URLPattern | URLResolver]:

View File

@@ -1,6 +1,6 @@
from django.urls import URLPattern, URLResolver, path
from api.views.v1_2 import AssetListViewV1_2, AssetViewV1_2
from anthias_server.api.views.v1_2 import AssetListViewV1_2, AssetViewV1_2
def get_url_patterns() -> list[URLPattern | URLResolver]:

View File

@@ -1,6 +1,6 @@
from django.urls import URLPattern, URLResolver, path
from api.views.v2 import (
from anthias_server.api.views.v2 import (
AssetContentViewV2,
AssetListViewV2,
AssetRecheckViewV2,

View File

@@ -16,20 +16,20 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from anthias_app.models import Asset
from api.helpers import save_active_assets_ordering
from api.serializers.mixins import (
from anthias_server.app.models import Asset
from anthias_server.api.helpers import save_active_assets_ordering
from anthias_server.api.serializers.mixins import (
BackupViewSerializerMixin,
PlaylistOrderSerializerMixin,
RebootViewSerializerMixin,
ShutdownViewSerializerMixin,
)
from celery_tasks import reboot_anthias, shutdown_anthias
from lib import backup_helper, diagnostics
from lib.auth import authorized
from lib.github import is_up_to_date
from lib.utils import connect_to_redis
from settings import ViewerPublisher, settings
from anthias_server.celery_tasks import reboot_anthias, shutdown_anthias
from anthias_server.lib import backup_helper, diagnostics
from anthias_server.lib.auth import authorized
from anthias_server.lib.github import is_up_to_date
from anthias_common.utils import connect_to_redis
from anthias_server.settings import ViewerPublisher, settings
logger = logging.getLogger(__name__)

View File

@@ -11,17 +11,17 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from anthias_app.models import Asset
from api.helpers import (
from anthias_server.app.models import Asset
from anthias_server.api.helpers import (
AssetCreationError,
parse_request,
)
from api.serializers import (
from anthias_server.api.serializers import (
AssetSerializer,
UpdateAssetSerializer,
)
from api.serializers.v1_1 import CreateAssetSerializerV1_1
from api.views.mixins import (
from anthias_server.api.serializers.v1_1 import CreateAssetSerializerV1_1
from anthias_server.api.views.mixins import (
AssetContentViewMixin,
AssetsControlViewMixin,
BackupViewMixin,
@@ -33,8 +33,8 @@ from api.views.mixins import (
RecoverViewMixin,
ShutdownViewMixin,
)
from lib.auth import authorized
from settings import ReplyCollector, ViewerPublisher
from anthias_server.lib.auth import authorized
from anthias_server.settings import ReplyCollector, ViewerPublisher
MODEL_STRING_EXAMPLE = """
Yes, that is just a string of JSON not JSON itself it will be parsed on the

View File

@@ -4,16 +4,16 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from anthias_app.models import Asset
from api.helpers import AssetCreationError, parse_request
from api.serializers import (
from anthias_server.app.models import Asset
from anthias_server.api.helpers import AssetCreationError, parse_request
from anthias_server.api.serializers import (
AssetSerializer,
UpdateAssetSerializer,
)
from api.serializers.v1_1 import CreateAssetSerializerV1_1
from api.views.mixins import DeleteAssetViewMixin
from api.views.v1 import V1_ASSET_REQUEST
from lib.auth import authorized
from anthias_server.api.serializers.v1_1 import CreateAssetSerializerV1_1
from anthias_server.api.views.mixins import DeleteAssetViewMixin
from anthias_server.api.views.v1 import V1_ASSET_REQUEST
from anthias_server.lib.auth import authorized
class AssetListViewV1_1(APIView):

View File

@@ -4,19 +4,19 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from anthias_app.models import Asset
from api.helpers import (
from anthias_server.app.models import Asset
from anthias_server.api.helpers import (
AssetCreationError,
get_active_asset_ids,
save_active_assets_ordering,
)
from api.serializers import (
from anthias_server.api.serializers import (
AssetSerializer,
UpdateAssetSerializer,
)
from api.serializers.v1_2 import CreateAssetSerializerV1_2
from api.views.mixins import DeleteAssetViewMixin
from lib.auth import authorized
from anthias_server.api.serializers.v1_2 import CreateAssetSerializerV1_2
from anthias_server.api.views.mixins import DeleteAssetViewMixin
from anthias_server.lib.auth import authorized
class AssetListViewV1_2(APIView):

View File

@@ -16,16 +16,16 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from anthias_app.helpers import add_default_assets, remove_default_assets
from anthias_app.models import Asset
from api.helpers import (
from anthias_server.app.helpers import add_default_assets, remove_default_assets
from anthias_server.app.models import Asset
from anthias_server.api.helpers import (
AssetCreationError,
get_active_asset_ids,
save_active_assets_ordering,
)
from lib.auth import hash_password
from lib.internal_auth import is_internal_request
from api.serializers.v2 import (
from anthias_server.lib.auth import hash_password
from anthias_common.internal_auth import is_internal_request
from anthias_server.api.serializers.v2 import (
AssetSerializerV2,
CreateAssetSerializerV2,
DeviceSettingsSerializerV2,
@@ -33,7 +33,7 @@ from api.serializers.v2 import (
UpdateAssetSerializerV2,
UpdateDeviceSettingsSerializerV2,
)
from api.views.mixins import (
from anthias_server.api.views.mixins import (
AssetContentViewMixin,
AssetsControlViewMixin,
BackupViewMixin,
@@ -45,17 +45,18 @@ from api.views.mixins import (
RecoverViewMixin,
ShutdownViewMixin,
)
from lib import device_helper, diagnostics
from lib.auth import authorized
from lib.github import is_up_to_date
from lib.utils import (
from anthias_common import device_helper
from anthias_server.lib import diagnostics
from anthias_server.lib.auth import authorized
from anthias_server.lib.github import is_up_to_date
from anthias_common.utils import (
connect_to_redis,
get_balena_device_info,
get_node_ip,
get_node_mac_address,
is_balena_app,
)
from settings import ViewerPublisher, settings
from anthias_server.settings import ViewerPublisher, settings
r = connect_to_redis()
@@ -78,7 +79,7 @@ _IP_REFRESH_DEBOUNCE_S = 12
def _resolve_node_ip() -> str:
"""Non-blocking IP-string resolver for the splash polling endpoint.
``lib.utils.get_node_ip()`` is the right primitive for one-shot
``anthias_common.utils.get_node_ip()`` is the right primitive for one-shot
server-rendered surfaces like ``/api/v2/info``: it publishes
``set_ip_addresses`` to host_agent and waits up to ~80s
(60s host_agent_ready + 20s ip_addresses_ready) for the result.
@@ -99,7 +100,7 @@ def _resolve_node_ip() -> str:
formatter below can stay shared with ``InfoViewV2``.
"""
if is_balena_app():
# Reuse the shared lib.utils helper so URL construction /
# Reuse the shared anthias_common.utils helper so URL construction /
# auth / headers stay in one place (extending it to accept a
# timeout was the smaller change). The bounded timeout is the
# load-bearing part of this fix — without it, a slow
@@ -165,7 +166,7 @@ def _resolve_node_ip() -> str:
# block waiting for completion.
_publish_refresh()
# Mirror ``lib.utils.get_node_ip()``'s ``MY_IP`` fallback.
# Mirror ``anthias_common.utils.get_node_ip()``'s ``MY_IP`` fallback.
# ``bin/upgrade_containers.sh`` exports the host's outbound IP
# into the server container via ``docker-compose.yml.tmpl``, so
# the splash can show *something* useful even when host_agent
@@ -439,10 +440,10 @@ class AssetRecheckViewV2(APIView):
return Response(status=status.HTTP_404_NOT_FOUND)
# Imported here to avoid a circular import at module load:
# celery_tasks imports api.* via Django's app registry, and
# anthias_server.celery_tasks imports api.* via Django's app registry, and
# importing it at the top of this view module pulls celery into
# the request path on every request even when not needed.
from celery_tasks import (
from anthias_server.celery_tasks import (
ASSET_RECHECK_QUEUE_DEBOUNCE_S,
asset_recheck_queue_key,
revalidate_asset_url,

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from anthias_app.models import Asset
from anthias_server.app.models import Asset
@admin.register(Asset)

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class AnthiasAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'anthias_app'
name = 'anthias_server.app'
label = 'anthias_app'

View File

@@ -7,10 +7,10 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.utils import timezone
from anthias_app.models import Asset
from lib.github import is_up_to_date
from lib.utils import get_video_duration
from settings import settings
from anthias_server.app.models import Asset
from anthias_server.lib.github import is_up_to_date
from anthias_common.utils import get_video_duration
from anthias_server.settings import settings
def template(
@@ -30,7 +30,7 @@ def template(
'default_streaming_duration'
]
context['template_settings'] = {
'imports': ['from lib.utils import template_handle_unicode'],
'imports': ['from anthias_common.utils import template_handle_unicode'],
'default_filters': ['template_handle_unicode'],
}
context['up_to_date'] = is_up_to_date()

View File

@@ -1,6 +1,6 @@
# Generated by Django 3.2.18 on 2024-08-23 18:45
import anthias_app.models
import anthias_server.app.models
from django.db import migrations, models
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Asset',
fields=[
('asset_id', models.TextField(default=anthias_app.models.generate_asset_id, editable=False, primary_key=True, serialize=False)),
('asset_id', models.TextField(default=anthias_server.app.models.generate_asset_id, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(blank=True, null=True)),
('uri', models.TextField(blank=True, null=True)),
('md5', models.TextField(blank=True, null=True)),

View File

@@ -1,6 +1,6 @@
# Generated by Django 4.2.30 on 2026-04-25 06:33
import anthias_app.models
import anthias_server.app.models
from django.db import migrations, models
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='asset',
name='play_days',
field=models.TextField(default=anthias_app.models._default_play_days),
field=models.TextField(default=anthias_server.app.models._default_play_days),
),
migrations.AddField(
model_name='asset',

View File

@@ -2,7 +2,7 @@ from django.urls import path, re_path
from . import views
app_name = 'anthias_app'
app_name = 'anthias_server.app'
urlpatterns = [
path('splash-page', views.splash_page, name='splash_page'),

View File

@@ -4,11 +4,11 @@ from django.shortcuts import redirect
from django.urls import reverse
from django.views.decorators.http import require_http_methods
from lib.auth import authorized
from lib.utils import (
from anthias_server.lib.auth import authorized
from anthias_common.utils import (
connect_to_redis,
)
from settings import settings
from anthias_server.settings import settings
from .helpers import (
template,
@@ -38,7 +38,7 @@ def login(request: HttpRequest) -> HttpResponse:
request.session['auth_username'] = username
request.session['auth_password'] = password
return redirect(reverse('anthias_app:react'))
return redirect(reverse('anthias_server.app:react'))
else:
messages.error(request, 'Invalid username or password')
return template(

View File

@@ -15,17 +15,17 @@ django.setup()
# Place imports that uses Django in this block.
from anthias_app.models import Asset # noqa: E402
from lib import diagnostics # noqa: E402
from lib.telemetry import send_telemetry # noqa: E402
from lib.utils import ( # noqa: E402
from anthias_server.app.models import Asset # noqa: E402
from anthias_server.lib import diagnostics # noqa: E402
from anthias_server.lib.telemetry import send_telemetry # noqa: E402
from anthias_common.utils import ( # noqa: E402
connect_to_redis,
is_balena_app,
reboot_via_balena_supervisor,
shutdown_via_balena_supervisor,
url_fails,
)
from settings import settings # noqa: E402
from anthias_server.settings import settings # noqa: E402
__author__ = 'Screenly, Inc'

View File

@@ -2,7 +2,7 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'anthias_django.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'anthias_server.django_project.settings')
django_asgi_app = get_asgi_application()
@@ -11,7 +11,7 @@ from channels.security.websocket import ( # noqa: E402
AllowedHostsOriginValidator,
)
from anthias_django.routing import websocket_urlpatterns # noqa: E402
from anthias_server.django_project.routing import websocket_urlpatterns # noqa: E402
# AllowedHostsOriginValidator gates WebSocket handshakes on the same
# ALLOWED_HOSTS list as Django's HTTP layer. With ALLOWED_HOSTS=['*']

View File

@@ -1,6 +1,6 @@
from django.urls import re_path
from anthias_app.consumers import AssetConsumer
from anthias_server.app.consumers import AssetConsumer
websocket_urlpatterns = [
re_path(r'^ws$', AssetConsumer.as_asgi()),

View File

@@ -1,5 +1,5 @@
"""
Django settings for anthias_django project.
Django settings for anthias_server.django_project project.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
@@ -16,11 +16,11 @@ from typing import Any
import pytz
from settings import settings as device_settings
from anthias_server.settings import settings as device_settings
# django_stubs_ext.monkeypatch() makes Django generic classes
# subscriptable at runtime, and the server side of this repo relies on
# that — anthias_app/admin.py defines `class AssetAdmin(admin.ModelAdmin
# that — anthias_server.app/admin.py defines `class AssetAdmin(admin.ModelAdmin
# [Asset])` at import time, which raises TypeError without the patch.
# Keep the import optional so the viewer image (and any future service
# that doesn't ship django-stubs-ext) can still load this settings
@@ -33,8 +33,8 @@ except ModuleNotFoundError as exc:
if exc.name != 'django_stubs_ext':
raise
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Repo root: src/anthias_server/django_project/settings.py → up 3 to repo root.
BASE_DIR = Path(__file__).resolve().parents[3]
# Quick-start development settings - unsuitable for production
@@ -85,7 +85,7 @@ ALLOWED_HOSTS = [
# on. Loaded by every service that calls django.setup() — server,
# celery, viewer, test.
INSTALLED_APPS = [
'anthias_app.apps.AnthiasAppConfig',
'anthias_server.app.apps.AnthiasAppConfig',
'django.contrib.contenttypes',
'django.contrib.auth',
]
@@ -100,7 +100,7 @@ if getenv('ANTHIAS_SERVICE') != 'viewer':
'channels',
'drf_spectacular',
'rest_framework',
'api.apps.ApiConfig',
'anthias_server.api.apps.ApiConfig',
'django.contrib.admin',
'django.contrib.sessions',
'django.contrib.messages',
@@ -119,7 +119,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'anthias_django.urls'
ROOT_URLCONF = 'anthias_server.django_project.urls'
TEMPLATES = [
{
@@ -139,7 +139,7 @@ TEMPLATES = [
},
]
ASGI_APPLICATION = 'anthias_django.asgi.application'
ASGI_APPLICATION = 'anthias_server.django_project.asgi.application'
CHANNEL_LAYERS = {
'default': {
@@ -276,7 +276,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'EXCEPTION_HANDLER': 'api.helpers.custom_exception_handler',
'EXCEPTION_HANDLER': 'anthias_server.api.helpers.custom_exception_handler',
# The project uses custom authentication classes,
# so we need to disable the default ones.
'DEFAULT_AUTHENTICATION_CLASSES': [],
@@ -286,7 +286,7 @@ SPECTACULAR_SETTINGS = {
'TITLE': 'Anthias API',
'VERSION': '2.0.0',
'PREPROCESSING_HOOKS': [
'api.api_docs_filter_spec.preprocessing_filter_spec'
'anthias_server.api.api_docs_filter_spec.preprocessing_filter_spec'
],
}

View File

@@ -1,4 +1,4 @@
"""anthias_django URL Configuration
"""anthias_server.django_project URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
@@ -21,8 +21,8 @@ from django.http import HttpRequest, HttpResponse
from django.urls import include, path, re_path
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from anthias_app import views_files
from lib.auth import authorized
from anthias_server.app import views_files
from anthias_server.lib.auth import authorized
class APIDocView(SpectacularRedocView):
@@ -38,7 +38,7 @@ class APIDocView(SpectacularRedocView):
urlpatterns = [
path('admin', admin.site.urls),
path('api/', include('api.urls')),
path('api/', include('anthias_server.api.urls')),
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', APIDocView.as_view(url_name='schema'), name='redoc'),
re_path(
@@ -51,7 +51,7 @@ urlpatterns = [
views_files.static_with_mime,
name='static_with_mime',
),
path('', include('anthias_app.urls')),
path('', include('anthias_server.app.urls')),
]
# @TODO: Write custom 403 and 404 pages.

View File

@@ -1,5 +1,5 @@
"""
WSGI config for anthias_django project.
WSGI config for anthias_server.django_project project.
It exposes the WSGI callable as a module-level variable named ``application``.
@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'anthias_django.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'anthias_server.django_project.settings')
application = get_wsgi_application()

View File

@@ -193,7 +193,7 @@ class BasicAuth(Auth):
from django.shortcuts import redirect
from django.urls import reverse
return redirect(reverse('anthias_app:login'))
return redirect(reverse('anthias_server.app:login'))
def update_settings(
self,
@@ -255,7 +255,7 @@ def authorized(
from django.http import HttpRequest
from rest_framework.request import Request
from settings import settings
from anthias_server.settings import settings
@wraps(orig)
def decorated(*args: P.args, **kwargs: P.kwargs) -> 'R | HttpResponse':

View File

@@ -5,9 +5,7 @@ import subprocess
import sys
from datetime import datetime
from lib import device_helper
from . import utils
from anthias_common import device_helper, utils
_CEC_QUERY_SCRIPT = """

View File

@@ -5,8 +5,8 @@ from requests import exceptions
from requests import get as requests_get
from requests import head as requests_head
from lib.diagnostics import get_git_hash, get_git_short_hash
from lib.utils import connect_to_redis
from anthias_server.lib.diagnostics import get_git_hash, get_git_short_hash
from anthias_common.utils import connect_to_redis
r = connect_to_redis()

View File

@@ -8,11 +8,11 @@ from collections import Counter
from requests import exceptions
from requests import post as requests_post
from anthias_app.models import Asset
from lib.device_helper import parse_cpu_info
from lib.diagnostics import get_git_branch, get_git_short_hash
from lib.utils import connect_to_redis, is_balena_app, is_ci
from settings import settings
from anthias_server.app.models import Asset
from anthias_common.device_helper import parse_cpu_info
from anthias_server.lib.diagnostics import get_git_branch, get_git_short_hash
from anthias_common.utils import connect_to_redis, is_balena_app, is_ci
from anthias_server.settings import settings
ANALYTICS_MEASURE_ID = 'G-S3VX8HTPK7'
ANALYTICS_API_SECRET = 'G8NcBpRIS9qBsOj3ODK8gw'

View File

@@ -7,7 +7,7 @@ import sys
def main() -> None:
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'anthias_django.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'anthias_server.django_project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@@ -8,13 +8,13 @@ from collections import UserDict
from os import getenv, path
from typing import TYPE_CHECKING, Any, ClassVar
from lib.auth import (
from anthias_server.lib.auth import (
Auth,
BasicAuth,
NoAuth,
_is_legacy_sha256,
)
from lib.errors import ReplyTimeoutError
from anthias_common.errors import ReplyTimeoutError
if TYPE_CHECKING:
import redis
@@ -232,7 +232,7 @@ class ViewerPublisher:
if self.INSTANCE is not None:
raise ValueError('An instance already exists!')
from lib.utils import connect_to_redis
from anthias_common.utils import connect_to_redis
self._redis: 'redis.Redis' = connect_to_redis()
@@ -274,7 +274,7 @@ class ReplyCollector:
if self.INSTANCE is not None:
raise ValueError('An instance already exists!')
from lib.utils import connect_to_redis
from anthias_common.utils import connect_to_redis
self._redis: 'redis.Redis' = connect_to_redis()

View File

@@ -12,15 +12,15 @@ import pydbus
import requests
import sh as sh
from settings import LISTEN, PORT, ReplySender, settings
from viewer.constants import EMPTY_PL_DELAY as EMPTY_PL_DELAY
from viewer.constants import SERVER_WAIT_TIMEOUT as SERVER_WAIT_TIMEOUT
from viewer.constants import SPLASH_DELAY as SPLASH_DELAY
from viewer.constants import SPLASH_PAGE_URL as SPLASH_PAGE_URL
from viewer.constants import STANDBY_SCREEN as STANDBY_SCREEN
from viewer.media_player import MediaPlayerProxy
from viewer.playback import navigate_to_asset, play_loop, skip_asset, stop_loop
from viewer.utils import (
from anthias_server.settings import LISTEN, PORT, ReplySender, settings
from anthias_viewer.constants import EMPTY_PL_DELAY as EMPTY_PL_DELAY
from anthias_viewer.constants import SERVER_WAIT_TIMEOUT as SERVER_WAIT_TIMEOUT
from anthias_viewer.constants import SPLASH_DELAY as SPLASH_DELAY
from anthias_viewer.constants import SPLASH_PAGE_URL as SPLASH_PAGE_URL
from anthias_viewer.constants import STANDBY_SCREEN as STANDBY_SCREEN
from anthias_viewer.media_player import MediaPlayerProxy
from anthias_viewer.playback import navigate_to_asset, play_loop, skip_asset, stop_loop
from anthias_viewer.utils import (
command_not_found,
get_skip_event,
sigalrm,
@@ -32,13 +32,13 @@ django.setup()
# Place imports that uses Django in this block.
from lib.internal_auth import INTERNAL_AUTH_HEADER, internal_auth_token # noqa: E402
from lib.utils import ( # noqa: E402
from anthias_common.internal_auth import INTERNAL_AUTH_HEADER, internal_auth_token # noqa: E402
from anthias_common.utils import ( # noqa: E402
connect_to_redis,
string_to_bool,
)
from viewer.messaging import ViewerSubscriber # noqa: E402
from viewer.scheduling import Scheduler # noqa: E402
from anthias_viewer.messaging import ViewerSubscriber # noqa: E402
from anthias_viewer.scheduling import Scheduler # noqa: E402
__author__ = 'Screenly, Inc'

View File

@@ -1,6 +1,6 @@
import logging
from viewer import main
from anthias_viewer import main
if __name__ == '__main__':
try:

View File

@@ -1,4 +1,4 @@
from settings import LISTEN, PORT
from anthias_server.settings import LISTEN, PORT
SPLASH_DELAY = 60 # secs
EMPTY_PL_DELAY = 5 # secs

View File

@@ -3,8 +3,8 @@ import os
import subprocess
from typing import ClassVar
from lib.device_helper import get_device_type
from settings import settings
from anthias_common.device_helper import get_device_type
from anthias_server.settings import settings
VIDEO_TIMEOUT = 20 # secs

View File

@@ -5,7 +5,7 @@ from typing import Any, Callable
import redis
from settings import VIEWER_CHANNEL
from anthias_server.settings import VIEWER_CHANNEL
class ViewerSubscriber(Thread):

View File

@@ -6,8 +6,8 @@ from typing import Any
from django.utils import timezone
from anthias_app.models import Asset
from settings import settings
from anthias_server.app.models import Asset
from anthias_server.settings import settings
# Re-evaluate windowed playlists at most this often. Day-of-week and
# time-of-day boundaries don't show up in start_date/end_date, so we

View File

@@ -7,8 +7,8 @@ from typing import Any
import requests
from lib.errors import SigalrmError
from settings import LISTEN, PORT
from anthias_common.errors import SigalrmError
from anthias_server.settings import LISTEN, PORT
WATCHDOG_PATH = '/tmp/anthias.watchdog'
@@ -24,7 +24,7 @@ def get_skip_event() -> threading.Event:
"""
Get the global skip event for instant asset switching.
"""
from viewer.playback import skip_event
from anthias_viewer.playback import skip_event
return skip_event

View File

@@ -9,7 +9,7 @@ fixtures themselves.
Three concerns are handled here, in order:
1. Force ``ENVIRONMENT=test`` so ``anthias_django/settings.py`` selects
1. Force ``ENVIRONMENT=test`` so ``anthias_server.django_project/settings.py`` selects
the SQLite test-DB branch (a repo-local path under ``BASE_DIR`` by
default; CI overrides via ``ANTHIAS_TEST_DB_PATH``).
@@ -21,11 +21,11 @@ Three concerns are handled here, in order:
active interpreter. The stubs let the import succeed; tests that
exercise dbus paths mock the relevant calls themselves.
3. Replace ``lib.utils.connect_to_redis`` with a dict-backed
3. Replace ``anthias_common.utils.connect_to_redis`` with a dict-backed
``MagicMock`` factory before any test module imports it, then expose
the same fake via an autouse fixture. Several modules call
``r = connect_to_redis()`` at import time
(``celery_tasks``, ``lib.github``, ``lib.telemetry``, ...); patching
(``anthias_server.celery_tasks``, ``anthias_server.lib.github``, ``anthias_server.lib.telemetry``, ...); patching
the factory once at conftest load time means the module-level ``r``
bindings hold a fake, not a client pointed at host ``redis``.
"""
@@ -148,7 +148,7 @@ def _make_fake_redis() -> MagicMock:
def _eval(script: str, numkeys: int, *args: Any) -> Any:
# Compare-and-delete is the only ``EVAL`` script in the
# codebase (sweep lock release in celery_tasks.py). Implement
# codebase (sweep lock release in anthias_server.celery_tasks.py). Implement
# that pattern directly; any other script becomes a no-op.
if "redis.call('get', KEYS[1])" in script and "'del'" in script:
keys = list(args[:numkeys])
@@ -195,14 +195,14 @@ def _make_fake_redis() -> MagicMock:
# Patch ``connect_to_redis`` at the source so the module-level
# ``r = connect_to_redis()`` bindings in celery_tasks / lib.github /
# lib.telemetry / api.views.mixins / viewer / etc. all resolve to the
# ``r = connect_to_redis()`` bindings in anthias_server.celery_tasks / anthias_server.lib.github /
# anthias_server.lib.telemetry / api.views.mixins / viewer / etc. all resolve to the
# fake the moment those modules are first imported.
_SESSION_FAKE_REDIS = _make_fake_redis()
def _patch_connect_to_redis() -> None:
import lib.utils as _lib_utils
import anthias_common.utils as _lib_utils
_lib_utils.connect_to_redis = lambda: _SESSION_FAKE_REDIS
@@ -221,7 +221,7 @@ def _ensure_assetdir() -> None:
Materialise the path once per session so those fixtures don't
``FileNotFoundError`` out before the test even runs.
"""
from settings import settings as _anthias_settings
from anthias_server.settings import settings as _anthias_settings
asset_dir = _anthias_settings.get('assetdir')
if asset_dir:
@@ -231,7 +231,7 @@ def _ensure_assetdir() -> None:
@pytest.fixture(autouse=True)
def _mock_redis(monkeypatch: pytest.MonkeyPatch) -> Iterator[MagicMock]:
"""
Replace ``lib.utils.connect_to_redis`` with a dict-backed
Replace ``anthias_common.utils.connect_to_redis`` with a dict-backed
``MagicMock`` for every test, including any module-level
``r = connect_to_redis()`` bindings that fixtures import indirectly.
@@ -240,20 +240,20 @@ def _mock_redis(monkeypatch: pytest.MonkeyPatch) -> Iterator[MagicMock]:
directly that takes precedence inside the per-test setup chain.
"""
fake = _make_fake_redis()
monkeypatch.setattr('lib.utils.connect_to_redis', lambda: fake)
monkeypatch.setattr('anthias_common.utils.connect_to_redis', lambda: fake)
# Replace already-bound ``r`` attributes on modules that called
# connect_to_redis() at import time. Only modules already in
# sys.modules are touched — others get the fake on first import via
# the conftest-level patch above.
for module_path in (
'anthias_app.views',
'api.views.mixins',
'api.views.v2',
'celery_tasks',
'lib.github',
'lib.telemetry',
'viewer',
'anthias_server.app.views',
'anthias_server.api.views.mixins',
'anthias_server.api.views.v2',
'anthias_server.celery_tasks',
'anthias_server.lib.github',
'anthias_server.lib.telemetry',
'anthias_viewer',
):
mod = sys.modules.get(module_path)
if mod is not None and hasattr(mod, 'r'):

View File

@@ -12,8 +12,8 @@ from selenium import webdriver
from selenium.common.exceptions import ElementNotVisibleException
from splinter import Browser
from anthias_app.models import Asset
from settings import settings
from anthias_server.app.models import Asset
from anthias_server.settings import settings
main_page_url = 'http://localhost:8080'
settings_url = 'http://foo:bar@localhost:8080/settings'

View File

@@ -6,8 +6,8 @@ import pytest
from django.contrib.sessions.backends.signed_cookies import SessionStore
from django.test import RequestFactory
from lib import auth
from lib.auth import (
from anthias_server.lib import auth
from anthias_server.lib.auth import (
Auth,
BasicAuth,
NoAuth,
@@ -396,7 +396,7 @@ def test_authorized_passthrough_when_no_auth(monkeypatch: Any) -> None:
"""If settings.auth is falsy, the wrapped view is called directly."""
fake_settings = MagicMock()
fake_settings.auth = None
monkeypatch.setattr('settings.settings', fake_settings)
monkeypatch.setattr('anthias_server.settings.settings', fake_settings)
@authorized
def view(request: Any) -> str:
@@ -416,7 +416,7 @@ def test_authorized_returns_auth_response_when_required(
fake_settings = MagicMock()
fake_settings.auth = auth_backend
monkeypatch.setattr('settings.settings', fake_settings)
monkeypatch.setattr('anthias_server.settings.settings', fake_settings)
@authorized
def view(request: Any) -> str:
@@ -431,7 +431,7 @@ def test_authorized_calls_view_when_authenticated(monkeypatch: Any) -> None:
auth_backend.authenticate_if_needed.return_value = None
fake_settings = MagicMock()
fake_settings.auth = auth_backend
monkeypatch.setattr('settings.settings', fake_settings)
monkeypatch.setattr('anthias_server.settings.settings', fake_settings)
@authorized
def view(request: Any) -> str:
@@ -444,7 +444,7 @@ def test_authorized_calls_view_when_authenticated(monkeypatch: Any) -> None:
def test_authorized_no_args_raises(monkeypatch: Any) -> None:
fake_settings = MagicMock()
fake_settings.auth = MagicMock()
monkeypatch.setattr('settings.settings', fake_settings)
monkeypatch.setattr('anthias_server.settings.settings', fake_settings)
@authorized
def view() -> str:
@@ -457,7 +457,7 @@ def test_authorized_no_args_raises(monkeypatch: Any) -> None:
def test_authorized_non_request_arg_raises(monkeypatch: Any) -> None:
fake_settings = MagicMock()
fake_settings.auth = MagicMock()
monkeypatch.setattr('settings.settings', fake_settings)
monkeypatch.setattr('anthias_server.settings.settings', fake_settings)
@authorized
def view(request: Any) -> str:

Some files were not shown because too many files have changed in this diff Show More