diff --git a/.github/workflows/deploy-website.yaml b/.github/workflows/deploy-website.yaml index 8f10d844..24b8b548 100644 --- a/.github/workflows/deploy-website.yaml +++ b/.github/workflows/deploy-website.yaml @@ -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 }} diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index d93eb8df..f637f30c 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -28,7 +28,7 @@ on: - '!docker/Dockerfile.dev' - '!.cursor/**' - '!.claude/**' - - '!host_agent.py' + - '!src/anthias_host_agent/**' workflow_dispatch: jobs: diff --git a/.github/workflows/generate-openapi-schema.yml b/.github/workflows/generate-openapi-schema.yml index 821a8466..d1ec1f52 100644 --- a/.github/workflows/generate-openapi-schema.yml +++ b/.github/workflows/generate-openapi-schema.yml @@ -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 diff --git a/.github/workflows/python-mypy.yaml b/.github/workflows/python-mypy.yaml index f3e7996f..001426ae 100644 --- a/.github/workflows/python-mypy.yaml +++ b/.github/workflows/python-mypy.yaml @@ -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. diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index b8657f06..fde61427 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -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 diff --git a/ansible/roles/anthias/templates/anthias-host-agent.service b/ansible/roles/anthias/templates/anthias-host-agent.service index 7b6efab2..8d7e6ae2 100644 --- a/ansible/roles/anthias/templates/anthias-host-agent.service +++ b/ansible/roles/anthias/templates/anthias-host-agent.service @@ -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 diff --git a/ansible/roles/anthias/templates/anthias_overrides.j2 b/ansible/roles/anthias/templates/anthias_overrides.j2 index 39faa6f8..a6d5faed 100644 --- a/ansible/roles/anthias/templates/anthias_overrides.j2 +++ b/ansible/roles/anthias/templates/anthias_overrides.j2 @@ -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 diff --git a/api/urls/__init__.py b/api/urls/__init__.py deleted file mode 100644 index 78a878e4..00000000 --- a/api/urls/__init__.py +++ /dev/null @@ -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(), -] diff --git a/bin/prepare_test_environment.sh b/bin/prepare_test_environment.sh index 2af25d1b..7d8b908f 100644 --- a/bin/prepare_test_environment.sh +++ b/bin/prepare_test_environment.sh @@ -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 diff --git a/bin/start_server.sh b/bin/start_server.sh index 617350e5..34af46e6 100755 --- a/bin/start_server.sh +++ b/bin/start_server.sh @@ -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 \ diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh index 3a391941..60c11ea0 100755 --- a/bin/start_viewer.sh +++ b/bin/start_viewer.sh @@ -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 diff --git a/docker-compose.balena.dev.yml.tmpl b/docker-compose.balena.dev.yml.tmpl index a65d72b5..1eb69024 100644 --- a/docker-compose.balena.dev.yml.tmpl +++ b/docker-compose.balena.dev.yml.tmpl @@ -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 diff --git a/docker-compose.balena.yml.tmpl b/docker-compose.balena.yml.tmpl index 78001bd7..6b0bf793 100644 --- a/docker-compose.balena.yml.tmpl +++ b/docker-compose.balena.yml.tmpl @@ -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 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b94249b2..01873026 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 1e84f130..b0b31dcc 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -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: diff --git a/docker-compose.yml.tmpl b/docker-compose.yml.tmpl index 786c204e..4f64c99b 100644 --- a/docker-compose.yml.tmpl +++ b/docker-compose.yml.tmpl @@ -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 diff --git a/docker/Dockerfile.server.j2 b/docker/Dockerfile.server.j2 index fb2d7e2f..9a5f460e 100644 --- a/docker/Dockerfile.server.j2 +++ b/docker/Dockerfile.server.j2 @@ -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"] diff --git a/docker/Dockerfile.test.j2 b/docker/Dockerfile.test.j2 index a6f060f9..1b57e41b 100644 --- a/docker/Dockerfile.test.j2 +++ b/docker/Dockerfile.test.j2 @@ -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" diff --git a/docker/Dockerfile.viewer.j2 b/docker/Dockerfile.viewer.j2 index 09460844..c16296fd 100644 --- a/docker/Dockerfile.viewer.j2 +++ b/docker/Dockerfile.viewer.j2 @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 740ee398..c2fc249f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/raspberry_pi_imager/bin/build-pi-imager-json.py b/raspberry_pi_imager/bin/build-pi-imager-json.py deleted file mode 100755 index cf50af1a..00000000 --- a/raspberry_pi_imager/bin/build-pi-imager-json.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -from raspberry_pi_imager.build_pi_imager_json import main - -if __name__ == '__main__': - main() diff --git a/ruff.toml b/ruff.toml index 51f37534..180e883c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -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] diff --git a/anthias_app/__init__.py b/src/anthias_common/__init__.py similarity index 100% rename from anthias_app/__init__.py rename to src/anthias_common/__init__.py diff --git a/lib/device_helper.py b/src/anthias_common/device_helper.py similarity index 100% rename from lib/device_helper.py rename to src/anthias_common/device_helper.py diff --git a/lib/errors.py b/src/anthias_common/errors.py similarity index 100% rename from lib/errors.py rename to src/anthias_common/errors.py diff --git a/lib/internal_auth.py b/src/anthias_common/internal_auth.py similarity index 100% rename from lib/internal_auth.py rename to src/anthias_common/internal_auth.py diff --git a/lib/utils.py b/src/anthias_common/utils.py similarity index 98% rename from lib/utils.py rename to src/anthias_common/utils.py index 7c70096f..4fa686df 100644 --- a/lib/utils.py +++ b/src/anthias_common/utils.py @@ -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 diff --git a/src/anthias_host_agent/__init__.py b/src/anthias_host_agent/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/anthias_host_agent/__init__.py @@ -0,0 +1 @@ + diff --git a/host_agent.py b/src/anthias_host_agent/__main__.py similarity index 100% rename from host_agent.py rename to src/anthias_host_agent/__main__.py diff --git a/anthias_app/management/__init__.py b/src/anthias_server/api/__init__.py similarity index 100% rename from anthias_app/management/__init__.py rename to src/anthias_server/api/__init__.py diff --git a/api/admin.py b/src/anthias_server/api/admin.py similarity index 100% rename from api/admin.py rename to src/anthias_server/api/admin.py diff --git a/api/api_docs_filter_spec.py b/src/anthias_server/api/api_docs_filter_spec.py similarity index 100% rename from api/api_docs_filter_spec.py rename to src/anthias_server/api/api_docs_filter_spec.py diff --git a/api/apps.py b/src/anthias_server/api/apps.py similarity index 70% rename from api/apps.py rename to src/anthias_server/api/apps.py index 66656fd2..a8b2278b 100644 --- a/api/apps.py +++ b/src/anthias_server/api/apps.py @@ -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' diff --git a/api/errors.py b/src/anthias_server/api/errors.py similarity index 100% rename from api/errors.py rename to src/anthias_server/api/errors.py diff --git a/api/helpers.py b/src/anthias_server/api/helpers.py similarity index 98% rename from api/helpers.py rename to src/anthias_server/api/helpers.py index d33ef461..ec8f717a 100644 --- a/api/helpers.py +++ b/src/anthias_server/api/helpers.py @@ -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): diff --git a/anthias_app/management/commands/__init__.py b/src/anthias_server/api/migrations/__init__.py similarity index 100% rename from anthias_app/management/commands/__init__.py rename to src/anthias_server/api/migrations/__init__.py diff --git a/api/serializers/__init__.py b/src/anthias_server/api/serializers/__init__.py similarity index 97% rename from api/serializers/__init__.py rename to src/anthias_server/api/serializers/__init__.py index 8a94d573..cd6e312f 100644 --- a/api/serializers/__init__.py +++ b/src/anthias_server/api/serializers/__init__.py @@ -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: diff --git a/api/serializers/mixins.py b/src/anthias_server/api/serializers/mixins.py similarity index 97% rename from api/serializers/mixins.py rename to src/anthias_server/api/serializers/mixins.py index 98ab4d7f..beb3b671 100644 --- a/api/serializers/mixins.py +++ b/src/anthias_server/api/serializers/mixins.py @@ -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, diff --git a/api/serializers/v1_1.py b/src/anthias_server/api/serializers/v1_1.py similarity index 98% rename from api/serializers/v1_1.py rename to src/anthias_server/api/serializers/v1_1.py index 2521e56d..8528b1e7 100644 --- a/api/serializers/v1_1.py +++ b/src/anthias_server/api/serializers/v1_1.py @@ -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, diff --git a/api/serializers/v1_2.py b/src/anthias_server/api/serializers/v1_2.py similarity index 94% rename from api/serializers/v1_2.py rename to src/anthias_server/api/serializers/v1_2.py index 490b8b1b..af6c9b62 100644 --- a/api/serializers/v1_2.py +++ b/src/anthias_server/api/serializers/v1_2.py @@ -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( diff --git a/api/serializers/v2.py b/src/anthias_server/api/serializers/v2.py similarity index 97% rename from api/serializers/v2.py rename to src/anthias_server/api/serializers/v2.py index 53ee5aa5..2518f9e6 100644 --- a/api/serializers/v2.py +++ b/src/anthias_server/api/serializers/v2.py @@ -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]: diff --git a/api/tests/__init__.py b/src/anthias_server/api/tests/__init__.py similarity index 100% rename from api/tests/__init__.py rename to src/anthias_server/api/tests/__init__.py diff --git a/api/tests/test_assets.py b/src/anthias_server/api/tests/test_assets.py similarity index 97% rename from api/tests/test_assets.py rename to src/anthias_server/api/tests/test_assets.py index 2882147c..064f6549 100644 --- a/api/tests/test_assets.py +++ b/src/anthias_server/api/tests/test_assets.py @@ -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: diff --git a/api/tests/test_common.py b/src/anthias_server/api/tests/test_common.py similarity index 100% rename from api/tests/test_common.py rename to src/anthias_server/api/tests/test_common.py diff --git a/api/tests/test_info_endpoints.py b/src/anthias_server/api/tests/test_info_endpoints.py similarity index 71% rename from api/tests/test_info_endpoints.py rename to src/anthias_server/api/tests/test_info_endpoints.py index fcde11e0..ae6978f3 100644 --- a/api/tests/test_info_endpoints.py +++ b/src/anthias_server/api/tests/test_info_endpoints.py @@ -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, diff --git a/api/tests/test_v1_endpoints.py b/src/anthias_server/api/tests/test_v1_endpoints.py similarity index 90% rename from api/tests/test_v1_endpoints.py rename to src/anthias_server/api/tests/test_v1_endpoints.py index a0ee9fbc..12f492ae 100644 --- a/api/tests/test_v1_endpoints.py +++ b/src/anthias_server/api/tests/test_v1_endpoints.py @@ -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 diff --git a/api/tests/test_v2_endpoints.py b/src/anthias_server/api/tests/test_v2_endpoints.py similarity index 93% rename from api/tests/test_v2_endpoints.py rename to src/anthias_server/api/tests/test_v2_endpoints.py index fed4751a..bc5418c6 100644 --- a/api/tests/test_v2_endpoints.py +++ b/src/anthias_server/api/tests/test_v2_endpoints.py @@ -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: diff --git a/src/anthias_server/api/urls/__init__.py b/src/anthias_server/api/urls/__init__.py new file mode 100644 index 00000000..699bf5e0 --- /dev/null +++ b/src/anthias_server/api/urls/__init__.py @@ -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(), +] diff --git a/api/urls/v1.py b/src/anthias_server/api/urls/v1.py similarity index 97% rename from api/urls/v1.py rename to src/anthias_server/api/urls/v1.py index cc31c820..f74ce5e9 100644 --- a/api/urls/v1.py +++ b/src/anthias_server/api/urls/v1.py @@ -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, diff --git a/api/urls/v1_1.py b/src/anthias_server/api/urls/v1_1.py similarity index 83% rename from api/urls/v1_1.py rename to src/anthias_server/api/urls/v1_1.py index 991aa563..f7b5689e 100644 --- a/api/urls/v1_1.py +++ b/src/anthias_server/api/urls/v1_1.py @@ -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]: diff --git a/api/urls/v1_2.py b/src/anthias_server/api/urls/v1_2.py similarity index 83% rename from api/urls/v1_2.py rename to src/anthias_server/api/urls/v1_2.py index 7b810cb0..5d5bc109 100644 --- a/api/urls/v1_2.py +++ b/src/anthias_server/api/urls/v1_2.py @@ -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]: diff --git a/api/urls/v2.py b/src/anthias_server/api/urls/v2.py similarity index 98% rename from api/urls/v2.py rename to src/anthias_server/api/urls/v2.py index fc501dd0..1106a477 100644 --- a/api/urls/v2.py +++ b/src/anthias_server/api/urls/v2.py @@ -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, diff --git a/anthias_app/migrations/__init__.py b/src/anthias_server/api/views/__init__.py similarity index 100% rename from anthias_app/migrations/__init__.py rename to src/anthias_server/api/views/__init__.py diff --git a/api/views/mixins.py b/src/anthias_server/api/views/mixins.py similarity index 95% rename from api/views/mixins.py rename to src/anthias_server/api/views/mixins.py index 833f245e..18a566b1 100644 --- a/api/views/mixins.py +++ b/src/anthias_server/api/views/mixins.py @@ -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__) diff --git a/api/views/v1.py b/src/anthias_server/api/views/v1.py similarity index 93% rename from api/views/v1.py rename to src/anthias_server/api/views/v1.py index 259e3de4..fc74dce7 100644 --- a/api/views/v1.py +++ b/src/anthias_server/api/views/v1.py @@ -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 diff --git a/api/views/v1_1.py b/src/anthias_server/api/views/v1_1.py similarity index 85% rename from api/views/v1_1.py rename to src/anthias_server/api/views/v1_1.py index 7a2f4c71..7b669e8a 100644 --- a/api/views/v1_1.py +++ b/src/anthias_server/api/views/v1_1.py @@ -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): diff --git a/api/views/v1_2.py b/src/anthias_server/api/views/v1_2.py similarity index 91% rename from api/views/v1_2.py rename to src/anthias_server/api/views/v1_2.py index c12dd939..6f0933e7 100644 --- a/api/views/v1_2.py +++ b/src/anthias_server/api/views/v1_2.py @@ -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): diff --git a/api/views/v2.py b/src/anthias_server/api/views/v2.py similarity index 96% rename from api/views/v2.py rename to src/anthias_server/api/views/v2.py index 19692645..ff18d4bc 100644 --- a/api/views/v2.py +++ b/src/anthias_server/api/views/v2.py @@ -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, diff --git a/anthias_django/__init__.py b/src/anthias_server/app/__init__.py similarity index 100% rename from anthias_django/__init__.py rename to src/anthias_server/app/__init__.py diff --git a/anthias_app/admin.py b/src/anthias_server/app/admin.py similarity index 91% rename from anthias_app/admin.py rename to src/anthias_server/app/admin.py index dfd1453f..be315c76 100644 --- a/anthias_app/admin.py +++ b/src/anthias_server/app/admin.py @@ -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) diff --git a/anthias_app/apps.py b/src/anthias_server/app/apps.py similarity index 68% rename from anthias_app/apps.py rename to src/anthias_server/app/apps.py index a6f7354b..3be21282 100644 --- a/anthias_app/apps.py +++ b/src/anthias_server/app/apps.py @@ -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' diff --git a/anthias_app/consumers.py b/src/anthias_server/app/consumers.py similarity index 100% rename from anthias_app/consumers.py rename to src/anthias_server/app/consumers.py diff --git a/anthias_app/helpers.py b/src/anthias_server/app/helpers.py similarity index 91% rename from anthias_app/helpers.py rename to src/anthias_server/app/helpers.py index bff2e5e5..581701a2 100644 --- a/anthias_app/helpers.py +++ b/src/anthias_server/app/helpers.py @@ -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() diff --git a/api/__init__.py b/src/anthias_server/app/management/__init__.py similarity index 100% rename from api/__init__.py rename to src/anthias_server/app/management/__init__.py diff --git a/api/migrations/__init__.py b/src/anthias_server/app/management/commands/__init__.py similarity index 100% rename from api/migrations/__init__.py rename to src/anthias_server/app/management/commands/__init__.py diff --git a/anthias_app/migrations/0001_initial.py b/src/anthias_server/app/migrations/0001_initial.py similarity index 86% rename from anthias_app/migrations/0001_initial.py rename to src/anthias_server/app/migrations/0001_initial.py index 6d3b0ce2..24c606e3 100644 --- a/anthias_app/migrations/0001_initial.py +++ b/src/anthias_server/app/migrations/0001_initial.py @@ -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)), diff --git a/anthias_app/migrations/0002_auto_20241015_1524.py b/src/anthias_server/app/migrations/0002_auto_20241015_1524.py similarity index 100% rename from anthias_app/migrations/0002_auto_20241015_1524.py rename to src/anthias_server/app/migrations/0002_auto_20241015_1524.py diff --git a/anthias_app/migrations/0003_asset_reachability.py b/src/anthias_server/app/migrations/0003_asset_reachability.py similarity index 100% rename from anthias_app/migrations/0003_asset_reachability.py rename to src/anthias_server/app/migrations/0003_asset_reachability.py diff --git a/anthias_app/migrations/0004_asset_schedule_fields.py b/src/anthias_server/app/migrations/0004_asset_schedule_fields.py similarity index 84% rename from anthias_app/migrations/0004_asset_schedule_fields.py rename to src/anthias_server/app/migrations/0004_asset_schedule_fields.py index 99958b58..0c328135 100644 --- a/anthias_app/migrations/0004_asset_schedule_fields.py +++ b/src/anthias_server/app/migrations/0004_asset_schedule_fields.py @@ -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', diff --git a/api/views/__init__.py b/src/anthias_server/app/migrations/__init__.py similarity index 100% rename from api/views/__init__.py rename to src/anthias_server/app/migrations/__init__.py diff --git a/anthias_app/models.py b/src/anthias_server/app/models.py similarity index 100% rename from anthias_app/models.py rename to src/anthias_server/app/models.py diff --git a/anthias_app/urls.py b/src/anthias_server/app/urls.py similarity index 88% rename from anthias_app/urls.py rename to src/anthias_server/app/urls.py index b1af7f77..98753726 100644 --- a/anthias_app/urls.py +++ b/src/anthias_server/app/urls.py @@ -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'), diff --git a/anthias_app/views.py b/src/anthias_server/app/views.py similarity index 90% rename from anthias_app/views.py rename to src/anthias_server/app/views.py index 77fd79fb..6a93a69e 100644 --- a/anthias_app/views.py +++ b/src/anthias_server/app/views.py @@ -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( diff --git a/anthias_app/views_files.py b/src/anthias_server/app/views_files.py similarity index 100% rename from anthias_app/views_files.py rename to src/anthias_server/app/views_files.py diff --git a/celery_tasks.py b/src/anthias_server/celery_tasks.py similarity index 97% rename from celery_tasks.py rename to src/anthias_server/celery_tasks.py index 0ad5055e..71081277 100755 --- a/celery_tasks.py +++ b/src/anthias_server/celery_tasks.py @@ -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' diff --git a/lib/__init__.py b/src/anthias_server/django_project/__init__.py similarity index 100% rename from lib/__init__.py rename to src/anthias_server/django_project/__init__.py diff --git a/anthias_django/asgi.py b/src/anthias_server/django_project/asgi.py similarity index 80% rename from anthias_django/asgi.py rename to src/anthias_server/django_project/asgi.py index 34605668..4a55619a 100644 --- a/anthias_django/asgi.py +++ b/src/anthias_server/django_project/asgi.py @@ -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=['*'] diff --git a/anthias_django/routing.py b/src/anthias_server/django_project/routing.py similarity index 66% rename from anthias_django/routing.py rename to src/anthias_server/django_project/routing.py index 214f1904..11741f31 100644 --- a/anthias_django/routing.py +++ b/src/anthias_server/django_project/routing.py @@ -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()), diff --git a/anthias_django/settings.py b/src/anthias_server/django_project/settings.py similarity index 93% rename from anthias_django/settings.py rename to src/anthias_server/django_project/settings.py index a31d4164..794f6dc3 100644 --- a/anthias_django/settings.py +++ b/src/anthias_server/django_project/settings.py @@ -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' ], } diff --git a/anthias_django/urls.py b/src/anthias_server/django_project/urls.py similarity index 86% rename from anthias_django/urls.py rename to src/anthias_server/django_project/urls.py index 56581cf0..cb6931ce 100644 --- a/anthias_django/urls.py +++ b/src/anthias_server/django_project/urls.py @@ -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. diff --git a/anthias_django/wsgi.py b/src/anthias_server/django_project/wsgi.py similarity index 66% rename from anthias_django/wsgi.py rename to src/anthias_server/django_project/wsgi.py index b68f5903..81f171d6 100644 --- a/anthias_django/wsgi.py +++ b/src/anthias_server/django_project/wsgi.py @@ -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() diff --git a/raspberry_pi_imager/__init__.py b/src/anthias_server/lib/__init__.py similarity index 100% rename from raspberry_pi_imager/__init__.py rename to src/anthias_server/lib/__init__.py diff --git a/lib/auth.py b/src/anthias_server/lib/auth.py similarity index 98% rename from lib/auth.py rename to src/anthias_server/lib/auth.py index d922bbf2..a6f550f3 100644 --- a/lib/auth.py +++ b/src/anthias_server/lib/auth.py @@ -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': diff --git a/lib/backup_helper.py b/src/anthias_server/lib/backup_helper.py similarity index 100% rename from lib/backup_helper.py rename to src/anthias_server/lib/backup_helper.py diff --git a/lib/diagnostics.py b/src/anthias_server/lib/diagnostics.py similarity index 98% rename from lib/diagnostics.py rename to src/anthias_server/lib/diagnostics.py index 973ae42e..b02c8ab0 100755 --- a/lib/diagnostics.py +++ b/src/anthias_server/lib/diagnostics.py @@ -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 = """ diff --git a/lib/github.py b/src/anthias_server/lib/github.py similarity index 98% rename from lib/github.py rename to src/anthias_server/lib/github.py index fd68b1e3..4faad20a 100644 --- a/lib/github.py +++ b/src/anthias_server/lib/github.py @@ -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() diff --git a/lib/telemetry.py b/src/anthias_server/lib/telemetry.py similarity index 93% rename from lib/telemetry.py rename to src/anthias_server/lib/telemetry.py index 7e95a6c7..1bbca80c 100644 --- a/lib/telemetry.py +++ b/src/anthias_server/lib/telemetry.py @@ -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' diff --git a/manage.py b/src/anthias_server/manage.py similarity index 86% rename from manage.py rename to src/anthias_server/manage.py index 4e56742f..69cf8b84 100755 --- a/manage.py +++ b/src/anthias_server/manage.py @@ -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: diff --git a/settings.py b/src/anthias_server/settings.py similarity index 98% rename from settings.py rename to src/anthias_server/settings.py index c587940e..ce9ad65e 100644 --- a/settings.py +++ b/src/anthias_server/settings.py @@ -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() diff --git a/viewer/__init__.py b/src/anthias_viewer/__init__.py similarity index 93% rename from viewer/__init__.py rename to src/anthias_viewer/__init__.py index 0c55f73d..49647a98 100644 --- a/viewer/__init__.py +++ b/src/anthias_viewer/__init__.py @@ -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' diff --git a/viewer/__main__.py b/src/anthias_viewer/__main__.py similarity index 82% rename from viewer/__main__.py rename to src/anthias_viewer/__main__.py index ced2b77a..98fd3b61 100755 --- a/viewer/__main__.py +++ b/src/anthias_viewer/__main__.py @@ -1,6 +1,6 @@ import logging -from viewer import main +from anthias_viewer import main if __name__ == '__main__': try: diff --git a/viewer/constants.py b/src/anthias_viewer/constants.py similarity index 80% rename from viewer/constants.py rename to src/anthias_viewer/constants.py index f335d455..9ba5730f 100644 --- a/viewer/constants.py +++ b/src/anthias_viewer/constants.py @@ -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 diff --git a/viewer/media_player.py b/src/anthias_viewer/media_player.py similarity index 97% rename from viewer/media_player.py rename to src/anthias_viewer/media_player.py index 06340265..841adc59 100644 --- a/viewer/media_player.py +++ b/src/anthias_viewer/media_player.py @@ -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 diff --git a/viewer/messaging.py b/src/anthias_viewer/messaging.py similarity index 98% rename from viewer/messaging.py rename to src/anthias_viewer/messaging.py index e91db86b..f9006129 100644 --- a/viewer/messaging.py +++ b/src/anthias_viewer/messaging.py @@ -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): diff --git a/viewer/playback.py b/src/anthias_viewer/playback.py similarity index 100% rename from viewer/playback.py rename to src/anthias_viewer/playback.py diff --git a/viewer/scheduling.py b/src/anthias_viewer/scheduling.py similarity index 98% rename from viewer/scheduling.py rename to src/anthias_viewer/scheduling.py index 1ff0c881..3a22a8e3 100644 --- a/viewer/scheduling.py +++ b/src/anthias_viewer/scheduling.py @@ -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 diff --git a/viewer/utils.py b/src/anthias_viewer/utils.py similarity index 88% rename from viewer/utils.py rename to src/anthias_viewer/utils.py index 4e33aca3..dd7485b9 100644 --- a/viewer/utils.py +++ b/src/anthias_viewer/utils.py @@ -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 diff --git a/conftest.py b/tests/conftest.py similarity index 89% rename from conftest.py rename to tests/conftest.py index b513cda8..71ca4fc6 100644 --- a/conftest.py +++ b/tests/conftest.py @@ -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'): diff --git a/tests/test_app.py b/tests/test_app.py index bf1357f3..889bc32c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -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' diff --git a/tests/test_auth.py b/tests/test_auth.py index 3a28432c..80c9e131 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -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: diff --git a/tests/test_backup_helper.py b/tests/test_backup_helper.py index cb8cea01..f76d8c7c 100644 --- a/tests/test_backup_helper.py +++ b/tests/test_backup_helper.py @@ -9,7 +9,7 @@ from unittest import mock import pytest -from lib.backup_helper import ( +from anthias_server.lib.backup_helper import ( create_backup, recover, static_dir, @@ -43,7 +43,7 @@ def backup_home() -> Iterator[str]: def test_get_backup_name(backup_home: str) -> None: dt = datetime(2016, 7, 19, 12, 42, 12) expected_archive_name = 'anthias-backup-2016-07-19T12-42-12.tar.gz' - with mock.patch('lib.backup_helper.datetime') as mock_datetime: + with mock.patch('anthias_server.lib.backup_helper.datetime') as mock_datetime: mock_datetime.now.return_value = dt archive_name = create_backup() assert archive_name == expected_archive_name diff --git a/tests/test_celery_tasks.py b/tests/test_celery_tasks.py index 82171256..3bde99ff 100644 --- a/tests/test_celery_tasks.py +++ b/tests/test_celery_tasks.py @@ -7,10 +7,10 @@ from unittest import mock import pytest -import celery_tasks as celery_tasks_module -from anthias_app.models import Asset -from celery_tasks import celery as celeryapp -from celery_tasks import ( +import anthias_server.celery_tasks as celery_tasks_module +from anthias_server.app.models import Asset +from anthias_server.celery_tasks import celery as celeryapp +from anthias_server.celery_tasks import ( ASSET_REVALIDATION_LOCK_KEY, asset_recheck_lock_key, cleanup, @@ -21,7 +21,7 @@ from celery_tasks import ( send_telemetry_task, shutdown_anthias, ) -from settings import settings +from anthias_server.settings import settings def _set_mtime(file_path: str, age_seconds: int) -> None: @@ -168,7 +168,7 @@ def test_get_display_power_writes_redis() -> None: with ( mock.patch.object(celery_tasks_module, 'r', fake_redis), mock.patch( - 'celery_tasks.diagnostics.get_display_power', + 'anthias_server.celery_tasks.diagnostics.get_display_power', return_value=True, ), ): @@ -179,7 +179,7 @@ def test_get_display_power_writes_redis() -> None: def test_send_telemetry_task_dispatches() -> None: - """The hourly Celery task is a thin wrapper over lib.telemetry.""" + """The hourly Celery task is a thin wrapper over anthias_server.lib.telemetry.""" with mock.patch.object(celery_tasks_module, 'send_telemetry') as mock_send: send_telemetry_task.apply() mock_send.assert_called_once_with() @@ -282,7 +282,7 @@ def eager_celery() -> None: @pytest.mark.django_db def test_sweep_marks_unreachable_when_url_fails(eager_celery: None) -> None: _make_revalidation_asset() - with mock.patch('celery_tasks.url_fails', return_value=True): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True): revalidate_asset_urls.apply() assert not Asset.objects.get(asset_id='a1').is_reachable @@ -290,7 +290,7 @@ def test_sweep_marks_unreachable_when_url_fails(eager_celery: None) -> None: @pytest.mark.django_db def test_sweep_marks_reachable_when_url_succeeds(eager_celery: None) -> None: _make_revalidation_asset(is_reachable=False) - with mock.patch('celery_tasks.url_fails', return_value=False): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=False): revalidate_asset_urls.apply() assert Asset.objects.get(asset_id='a1').is_reachable @@ -301,7 +301,7 @@ def test_sweep_updates_last_reachability_check(eager_celery: None) -> None: _make_revalidation_asset() before = timezone.now() - with mock.patch('celery_tasks.url_fails', return_value=False): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=False): revalidate_asset_urls.apply() last = Asset.objects.get(asset_id='a1').last_reachability_check assert last is not None @@ -311,7 +311,7 @@ def test_sweep_updates_last_reachability_check(eager_celery: None) -> None: @pytest.mark.django_db def test_sweep_skips_disabled_assets(eager_celery: None) -> None: _make_revalidation_asset(is_enabled=False, is_reachable=True) - with mock.patch('celery_tasks.url_fails', return_value=True) as m: + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True) as m: revalidate_asset_urls.apply() m.assert_not_called() assert Asset.objects.get(asset_id='a1').is_reachable @@ -320,7 +320,7 @@ def test_sweep_skips_disabled_assets(eager_celery: None) -> None: @pytest.mark.django_db def test_sweep_skips_processing_assets(eager_celery: None) -> None: _make_revalidation_asset(is_processing=True) - with mock.patch('celery_tasks.url_fails', return_value=True) as m: + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True) as m: revalidate_asset_urls.apply() m.assert_not_called() assert Asset.objects.get(asset_id='a1').is_reachable @@ -335,7 +335,7 @@ def test_sweep_skips_skip_asset_check_assets_entirely( exposes that field as 'last check' and writing it without an actual probe would advertise a check that never happened.""" _make_revalidation_asset(skip_asset_check=True) - with mock.patch('celery_tasks.url_fails') as m: + with mock.patch('anthias_server.celery_tasks.url_fails') as m: revalidate_asset_urls.apply() m.assert_not_called() assert Asset.objects.get(asset_id='a1').is_reachable @@ -349,7 +349,7 @@ def test_sweep_local_file_existence_check(eager_celery: None) -> None: local = fh.name try: _make_revalidation_asset(uri=local) - with mock.patch('celery_tasks.url_fails') as m: + with mock.patch('anthias_server.celery_tasks.url_fails') as m: revalidate_asset_urls.apply() m.assert_not_called() assert Asset.objects.get(asset_id='a1').is_reachable @@ -374,7 +374,7 @@ def test_sweep_probe_exception_does_not_kill_sweep( raise RuntimeError('synthetic') return False - with mock.patch('celery_tasks.url_fails', side_effect=fake_url_fails): + with mock.patch('anthias_server.celery_tasks.url_fails', side_effect=fake_url_fails): revalidate_asset_urls.apply() # 'boom' is left as-is (we don't have a probe result to write), @@ -392,7 +392,7 @@ def test_sweep_lock_prevents_overlap(eager_celery: None) -> None: _make_revalidation_asset() # Pre-acquire the lock to simulate a sweep already in flight. celery_tasks_module.r.set(ASSET_REVALIDATION_LOCK_KEY, 'someone-else') - with mock.patch('celery_tasks.url_fails', return_value=True) as m: + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True) as m: revalidate_asset_urls.apply() # The sweep saw the lock and exited without probing. m.assert_not_called() @@ -415,7 +415,7 @@ def test_sweep_lock_release_does_not_clobber_different_holder( celery_tasks_module.r.set(ASSET_REVALIDATION_LOCK_KEY, 'someone-else') return False # url_fails return — asset is reachable - with mock.patch('celery_tasks.url_fails', side_effect=steal_during_sweep): + with mock.patch('anthias_server.celery_tasks.url_fails', side_effect=steal_during_sweep): revalidate_asset_urls.apply() # Compare-and-delete saw a token mismatch and left the lock alone. @@ -430,7 +430,7 @@ def test_sweep_lock_released_after_clean_run(eager_celery: None) -> None: """The finally clause must release the lock so the next beat tick can run.""" _make_revalidation_asset() - with mock.patch('celery_tasks.url_fails', return_value=False): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=False): revalidate_asset_urls.apply() assert celery_tasks_module.r.get(ASSET_REVALIDATION_LOCK_KEY) is None @@ -471,7 +471,7 @@ def _make_recheck_asset(**kwargs: object) -> Asset: def test_recheck_no_op_when_asset_does_not_exist( eager_celery_recheck: None, ) -> None: - with mock.patch('celery_tasks.url_fails') as m: + with mock.patch('anthias_server.celery_tasks.url_fails') as m: revalidate_asset_url.apply(args=('nope',)) m.assert_not_called() assert celery_tasks_module.r.get(asset_recheck_lock_key('nope')) is None @@ -480,7 +480,7 @@ def test_recheck_no_op_when_asset_does_not_exist( @pytest.mark.django_db def test_recheck_flips_is_reachable(eager_celery_recheck: None) -> None: _make_recheck_asset(is_reachable=True) - with mock.patch('celery_tasks.url_fails', return_value=True): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True): revalidate_asset_url.apply(args=('a1',)) assert not Asset.objects.get(asset_id='a1').is_reachable @@ -495,7 +495,7 @@ def test_recheck_lock_prevents_back_to_back_probes( _make_recheck_asset(is_reachable=False) # Pre-acquire the cooldown lock to simulate a recent probe. celery_tasks_module.r.set(asset_recheck_lock_key('a1'), '1') - with mock.patch('celery_tasks.url_fails', return_value=False) as m: + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=False) as m: revalidate_asset_url.apply(args=('a1',)) m.assert_not_called() assert not Asset.objects.get(asset_id='a1').is_reachable @@ -509,7 +509,7 @@ def test_recheck_acquires_lock_when_running( gate is what prevents concurrent ffprobe calls for the same asset across workers.""" _make_recheck_asset() - with mock.patch('celery_tasks.url_fails', return_value=False): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=False): revalidate_asset_url.apply(args=('a1',)) assert celery_tasks_module.r.get(asset_recheck_lock_key('a1')) == '1' @@ -519,7 +519,7 @@ def test_recheck_skips_disabled_asset( eager_celery_recheck: None, ) -> None: _make_recheck_asset(is_enabled=False, is_reachable=True) - with mock.patch('celery_tasks.url_fails', return_value=True) as m: + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True) as m: revalidate_asset_url.apply(args=('a1',)) m.assert_not_called() assert Asset.objects.get(asset_id='a1').is_reachable @@ -531,7 +531,7 @@ def test_recheck_skips_processing_asset( eager_celery_recheck: None, ) -> None: _make_recheck_asset(is_processing=True) - with mock.patch('celery_tasks.url_fails', return_value=True) as m: + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=True) as m: revalidate_asset_url.apply(args=('a1',)) m.assert_not_called() assert Asset.objects.get(asset_id='a1').is_reachable @@ -545,7 +545,7 @@ def test_recheck_skips_skip_asset_check_asset( """Operator opted out of validation; matches sweep behavior of not touching is_reachable / last_reachability_check.""" _make_recheck_asset(skip_asset_check=True, is_reachable=True) - with mock.patch('celery_tasks.url_fails') as m: + with mock.patch('anthias_server.celery_tasks.url_fails') as m: revalidate_asset_url.apply(args=('a1',)) m.assert_not_called() assert Asset.objects.get(asset_id='a1').last_reachability_check is None @@ -558,6 +558,6 @@ def test_recheck_runs_when_no_lock_held( ) -> None: """No lock held → SETNX succeeds → probe runs.""" _make_recheck_asset(is_reachable=False) - with mock.patch('celery_tasks.url_fails', return_value=False): + with mock.patch('anthias_server.celery_tasks.url_fails', return_value=False): revalidate_asset_url.apply(args=('a1',)) assert Asset.objects.get(asset_id='a1').is_reachable diff --git a/tests/test_device_helper.py b/tests/test_device_helper.py index a0825c79..97b4b1da 100644 --- a/tests/test_device_helper.py +++ b/tests/test_device_helper.py @@ -2,7 +2,7 @@ from unittest import mock import pytest -from lib import device_helper +from anthias_common import device_helper PI4_CPUINFO = """\ diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index e777720e..cc38cc68 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -5,7 +5,7 @@ from unittest import mock import pytest -from lib import diagnostics +from anthias_server.lib import diagnostics @pytest.mark.parametrize( @@ -83,7 +83,7 @@ def test_get_debian_version_missing_file() -> None: def test_get_raspberry_code_returns_hardware() -> None: with mock.patch( - 'lib.diagnostics.device_helper.parse_cpu_info', + 'anthias_server.lib.diagnostics.device_helper.parse_cpu_info', return_value={'hardware': 'BCM2711', 'model': 'Pi 4'}, ): assert diagnostics.get_raspberry_code() == 'BCM2711' @@ -91,14 +91,14 @@ def test_get_raspberry_code_returns_hardware() -> None: def test_get_raspberry_code_unknown() -> None: with mock.patch( - 'lib.diagnostics.device_helper.parse_cpu_info', return_value={} + 'anthias_server.lib.diagnostics.device_helper.parse_cpu_info', return_value={} ): assert diagnostics.get_raspberry_code() == 'Unknown' def test_get_raspberry_model_returns_model() -> None: with mock.patch( - 'lib.diagnostics.device_helper.parse_cpu_info', + 'anthias_server.lib.diagnostics.device_helper.parse_cpu_info', return_value={'model': 'Raspberry Pi 4 Model B'}, ): assert diagnostics.get_raspberry_model() == 'Raspberry Pi 4 Model B' @@ -106,7 +106,7 @@ def test_get_raspberry_model_returns_model() -> None: def test_get_raspberry_model_unknown() -> None: with mock.patch( - 'lib.diagnostics.device_helper.parse_cpu_info', return_value={} + 'anthias_server.lib.diagnostics.device_helper.parse_cpu_info', return_value={} ): assert diagnostics.get_raspberry_model() == 'Unknown' @@ -156,7 +156,7 @@ def test_get_display_power_subprocess_timeout() -> None: def test_try_connectivity_all_succeed() -> None: - with mock.patch('lib.diagnostics.utils.url_fails', return_value=False): + with mock.patch('anthias_server.lib.diagnostics.utils.url_fails', return_value=False): results = diagnostics.try_connectivity() assert len(results) == 4 for line in results: @@ -164,7 +164,7 @@ def test_try_connectivity_all_succeed() -> None: def test_try_connectivity_all_fail() -> None: - with mock.patch('lib.diagnostics.utils.url_fails', return_value=True): + with mock.patch('anthias_server.lib.diagnostics.utils.url_fails', return_value=True): results = diagnostics.try_connectivity() assert len(results) == 4 for line in results: @@ -175,7 +175,7 @@ def test_try_connectivity_mixed() -> None: # Alternate True/False/True/False across the four URLs. side_effect = [True, False, True, False] with mock.patch( - 'lib.diagnostics.utils.url_fails', side_effect=side_effect + 'anthias_server.lib.diagnostics.utils.url_fails', side_effect=side_effect ): results = diagnostics.try_connectivity() assert results[0].endswith(': Error') diff --git a/tests/test_github.py b/tests/test_github.py index cc1b7ccb..1ebc0e69 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock import pytest from requests import exceptions as requests_exceptions -from lib import github +from anthias_server.lib import github logging.disable(logging.CRITICAL) diff --git a/tests/test_media_player.py b/tests/test_media_player.py index decc60bb..ed941dc2 100644 --- a/tests/test_media_player.py +++ b/tests/test_media_player.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch import pytest -from viewer.media_player import ( +from anthias_viewer.media_player import ( MPVMediaPlayer, MediaPlayerProxy, VLCMediaPlayer, @@ -26,9 +26,9 @@ def mpv(monkeypatch: pytest.MonkeyPatch) -> Iterator[_MPVFixtures]: fixtures = _MPVFixtures() fixtures.player = MPVMediaPlayer() - patch_settings = patch('viewer.media_player.settings') + patch_settings = patch('anthias_viewer.media_player.settings') patch_device_type = patch( - 'viewer.media_player.get_device_type', return_value='pi4' + 'anthias_viewer.media_player.get_device_type', return_value='pi4' ) fixtures.mock_settings = patch_settings.start() @@ -42,7 +42,7 @@ def mpv(monkeypatch: pytest.MonkeyPatch) -> Iterator[_MPVFixtures]: patch_device_type.stop() -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_play_invokes_popen_with_expected_args( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -65,7 +65,7 @@ def test_play_invokes_popen_with_expected_args( ) -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_play_pins_1080p_mode_on_pi4_64( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -80,7 +80,7 @@ def test_play_pins_1080p_mode_on_pi4_64( assert '--hwdec=v4l2m2m-copy' not in args[0] -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_play_pins_1080p_mode_on_pi5( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -93,7 +93,7 @@ def test_play_pins_1080p_mode_on_pi5( assert '--vd-lavc-threads=4' in args[0] -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_play_does_not_pin_mode_on_x86( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -106,7 +106,7 @@ def test_play_does_not_pin_mode_on_x86( assert '--vd-lavc-threads=4' not in args[0] -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_play_uses_local_audio_device_when_configured( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -118,7 +118,7 @@ def test_play_uses_local_audio_device_when_configured( assert '--audio-device=alsa/plughw:CARD=Headphones' in args[0] -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_play_reloads_settings_each_call( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -127,7 +127,7 @@ def test_play_reloads_settings_each_call( mpv.mock_settings.load.assert_called_once() -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_is_playing_returns_true_when_process_running( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -141,7 +141,7 @@ def test_is_playing_returns_true_when_process_running( assert mpv.player.is_playing() -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_is_playing_returns_false_when_process_finished( mock_popen: Any, mpv: _MPVFixtures ) -> None: @@ -159,7 +159,7 @@ def test_is_playing_returns_false_when_no_process(mpv: _MPVFixtures) -> None: assert not mpv.player.is_playing() -@patch('viewer.media_player.subprocess.Popen') +@patch('anthias_viewer.media_player.subprocess.Popen') def test_stop_terminates_process(mock_popen: Any, mpv: _MPVFixtures) -> None: mock_process = MagicMock() mock_popen.return_value = mock_process @@ -174,7 +174,7 @@ def test_stop_terminates_process(mock_popen: Any, mpv: _MPVFixtures) -> None: @pytest.fixture def alsa_settings() -> Iterator[Any]: - patch_settings = patch('viewer.media_player.settings') + patch_settings = patch('anthias_viewer.media_player.settings') mock_settings = patch_settings.start() try: yield mock_settings @@ -184,7 +184,7 @@ def alsa_settings() -> Iterator[Any]: def test_local_on_pi5_uses_hdmi_card(alsa_settings: Any) -> None: alsa_settings.__getitem__.return_value = 'local' - with patch('viewer.media_player.get_device_type', return_value='pi5'): + with patch('anthias_viewer.media_player.get_device_type', return_value='pi5'): assert get_alsa_audio_device() == 'default:CARD=vc4hdmi0' @@ -194,7 +194,7 @@ def test_local_on_other_pi_uses_headphones( ) -> None: alsa_settings.__getitem__.return_value = 'local' with patch( - 'viewer.media_player.get_device_type', + 'anthias_viewer.media_player.get_device_type', return_value=device_type, ): assert get_alsa_audio_device() == 'plughw:CARD=Headphones' @@ -206,7 +206,7 @@ def test_hdmi_on_pi4_pi5_uses_vc4hdmi0( ) -> None: alsa_settings.__getitem__.return_value = 'hdmi' with patch( - 'viewer.media_player.get_device_type', + 'anthias_viewer.media_player.get_device_type', return_value=device_type, ): assert get_alsa_audio_device() == 'default:CARD=vc4hdmi0' @@ -218,7 +218,7 @@ def test_hdmi_on_pi1_pi2_pi3_uses_vc4hdmi( ) -> None: alsa_settings.__getitem__.return_value = 'hdmi' with patch( - 'viewer.media_player.get_device_type', + 'anthias_viewer.media_player.get_device_type', return_value=device_type, ): assert get_alsa_audio_device() == 'default:CARD=vc4hdmi' @@ -226,7 +226,7 @@ def test_hdmi_on_pi1_pi2_pi3_uses_vc4hdmi( def test_hdmi_on_x86_falls_back_to_hid(alsa_settings: Any) -> None: alsa_settings.__getitem__.return_value = 'hdmi' - with patch('viewer.media_player.get_device_type', return_value='x86'): + with patch('anthias_viewer.media_player.get_device_type', return_value='x86'): assert get_alsa_audio_device() == 'default:CARD=HID' @@ -248,9 +248,9 @@ def vlc() -> Iterator[_VLCFixtures]: fixtures.mock_vlc_player.get_media.return_value = fixtures.mock_media fixtures.player.player = fixtures.mock_vlc_player - patch_settings = patch('viewer.media_player.settings') + patch_settings = patch('anthias_viewer.media_player.settings') patch_device_type = patch( - 'viewer.media_player.get_device_type', return_value='pi4' + 'anthias_viewer.media_player.get_device_type', return_value='pi4' ) fixtures.mock_settings = patch_settings.start() @@ -287,7 +287,7 @@ def test_get_instance_returns_vlc_for_pi_devices( MediaPlayerProxy.INSTANCE = None with ( patch( - 'viewer.media_player.get_device_type', + 'anthias_viewer.media_player.get_device_type', return_value=device_type, ), patch.dict('os.environ', {'DEVICE_TYPE': device_type}), @@ -303,7 +303,7 @@ def test_get_instance_returns_mpv_for_pi5_and_x86( ) -> None: MediaPlayerProxy.INSTANCE = None with patch( - 'viewer.media_player.get_device_type', + 'anthias_viewer.media_player.get_device_type', return_value=device_type, ): instance = MediaPlayerProxy.get_instance() @@ -313,14 +313,14 @@ def test_get_instance_returns_mpv_for_pi5_and_x86( def test_get_instance_returns_mpv_for_pi4_64(reset_media_proxy: None) -> None: MediaPlayerProxy.INSTANCE = None with ( - patch('viewer.media_player.get_device_type', return_value='pi4'), + patch('anthias_viewer.media_player.get_device_type', return_value='pi4'), patch.dict('os.environ', {'DEVICE_TYPE': 'pi4-64'}), ): instance = MediaPlayerProxy.get_instance() assert isinstance(instance, MPVMediaPlayer) -@patch('viewer.media_player.get_device_type', return_value='pi5') +@patch('anthias_viewer.media_player.get_device_type', return_value='pi5') def test_get_instance_returns_same_instance( _: Any, reset_media_proxy: None ) -> None: diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 78919d1d..d180be8b 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -7,15 +7,15 @@ from unittest.mock import MagicMock import pytest import redis -import settings as settings_module -from lib.errors import ReplyTimeoutError -from settings import ( +from anthias_common.errors import ReplyTimeoutError +from anthias_server import settings as settings_module +from anthias_server.settings import ( REPLY_KEY_PREFIX, ReplyCollector, ReplySender, ViewerPublisher, ) -from viewer.messaging import ViewerSubscriber +from anthias_viewer.messaging import ViewerSubscriber logging.disable(logging.CRITICAL) @@ -53,13 +53,13 @@ def test_viewer_publisher_get_instance_creates_once() -> None: try: # connect_to_redis is called inside __init__; mock it. with mock.patch( - 'lib.utils.connect_to_redis', return_value=MagicMock() + 'anthias_common.utils.connect_to_redis', return_value=MagicMock() ): inst = ViewerPublisher.get_instance() assert inst is ViewerPublisher.INSTANCE # Calling get_instance again returns the cached instance. with mock.patch( - 'lib.utils.connect_to_redis', return_value=MagicMock() + 'anthias_common.utils.connect_to_redis', return_value=MagicMock() ): assert ViewerPublisher.get_instance() is inst finally: @@ -164,12 +164,12 @@ def test_reply_collector_get_instance_creates_once() -> None: ReplyCollector.INSTANCE = None try: with mock.patch( - 'lib.utils.connect_to_redis', return_value=MagicMock() + 'anthias_common.utils.connect_to_redis', return_value=MagicMock() ): inst = ReplyCollector.get_instance() assert inst is ReplyCollector.INSTANCE with mock.patch( - 'lib.utils.connect_to_redis', return_value=MagicMock() + 'anthias_common.utils.connect_to_redis', return_value=MagicMock() ): assert ReplyCollector.get_instance() is inst finally: @@ -285,7 +285,7 @@ def test_subscriber_run_signals_ready_then_exits_on_loop_break( sleep_calls.append(delay) raise SystemExit # break out of while True - with mock.patch('viewer.messaging.sleep', side_effect=fake_sleep): + with mock.patch('anthias_viewer.messaging.sleep', side_effect=fake_sleep): with pytest.raises(SystemExit): sub.run() diff --git a/tests/test_recheck_endpoint.py b/tests/test_recheck_endpoint.py index 2a7ec9cc..ec439e31 100644 --- a/tests/test_recheck_endpoint.py +++ b/tests/test_recheck_endpoint.py @@ -17,11 +17,11 @@ from unittest import mock import pytest from django.test import Client -import api.views.v2 as v2_module -from anthias_app.models import Asset -from celery_tasks import asset_recheck_queue_key -from lib.internal_auth import INTERNAL_AUTH_HEADER, internal_auth_token -from settings import settings as anthias_settings +import anthias_server.api.views.v2 as v2_module +from anthias_server.app.models import Asset +from anthias_server.celery_tasks import asset_recheck_queue_key +from anthias_common.internal_auth import INTERNAL_AUTH_HEADER, internal_auth_token +from anthias_server.settings import settings as anthias_settings @pytest.fixture(autouse=True) @@ -53,7 +53,7 @@ def _make(**kwargs: object) -> Asset: @pytest.mark.django_db def test_returns_404_for_unknown_asset() -> None: - with mock.patch('celery_tasks.revalidate_asset_url.delay') as m: + with mock.patch('anthias_server.celery_tasks.revalidate_asset_url.delay') as m: response = Client().post( '/api/v2/assets/nope/recheck', headers=_auth_headers(), @@ -66,7 +66,7 @@ def test_returns_404_for_unknown_asset() -> None: def test_enqueues_task_when_no_lock_held() -> None: """Fresh asset, no recent recheck: SETNX succeeds → enqueue.""" _make() - with mock.patch('celery_tasks.revalidate_asset_url.delay') as m: + with mock.patch('anthias_server.celery_tasks.revalidate_asset_url.delay') as m: response = Client().post( '/api/v2/assets/a1/recheck', headers=_auth_headers(), @@ -86,7 +86,7 @@ def test_skips_enqueue_when_queue_debounce_held() -> None: # Pre-acquire the queue-debounce key on the same fake the # endpoint reads from. v2_module.r.set(asset_recheck_queue_key('a1'), '1') - with mock.patch('celery_tasks.revalidate_asset_url.delay') as m: + with mock.patch('anthias_server.celery_tasks.revalidate_asset_url.delay') as m: response = Client().post( '/api/v2/assets/a1/recheck', headers=_auth_headers(), @@ -103,7 +103,7 @@ def test_back_to_back_calls_only_enqueue_once() -> None: after the task finishes, so two near-simultaneous endpoint hits would both read the stale value and each enqueue.""" _make() - with mock.patch('celery_tasks.revalidate_asset_url.delay') as m: + with mock.patch('anthias_server.celery_tasks.revalidate_asset_url.delay') as m: r1 = Client().post( '/api/v2/assets/a1/recheck', headers=_auth_headers() ) @@ -118,7 +118,7 @@ def test_back_to_back_calls_only_enqueue_once() -> None: @pytest.mark.django_db def test_missing_internal_auth_is_forbidden() -> None: _make() - with mock.patch('celery_tasks.revalidate_asset_url.delay') as m: + with mock.patch('anthias_server.celery_tasks.revalidate_asset_url.delay') as m: response = Client().post('/api/v2/assets/a1/recheck') assert response.status_code == 403 m.assert_not_called() @@ -129,7 +129,7 @@ def test_internal_auth_works_without_basic_auth() -> None: """Viewer has no operator BasicAuth credentials; the internal header is sufficient for this side-effect-only endpoint.""" _make() - with mock.patch('celery_tasks.revalidate_asset_url.delay') as m: + with mock.patch('anthias_server.celery_tasks.revalidate_asset_url.delay') as m: response = Client().post( '/api/v2/assets/a1/recheck', headers=_auth_headers(), diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index f6b3baf3..776a9181 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -8,9 +8,9 @@ import pytest import time_machine from django.utils import timezone -from anthias_app.models import Asset -from settings import settings -from viewer.scheduling import ( +from anthias_server.app.models import Asset +from anthias_server.settings import settings +from anthias_viewer.scheduling import ( Scheduler, WINDOWED_DEADLINE_CAP_SECONDS, generate_asset_list, diff --git a/tests/test_settings.py b/tests/test_settings.py index e24a78a2..1ffc5e38 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -64,13 +64,22 @@ def fake_settings(raw: str) -> Iterator[tuple[Any, Any]]: # `settings` cleanly would leave the module cached, and `import # settings` here would skip __init__ entirely — silently accepting # any config (including the broken-by-design fixture). - sys.modules.pop('settings', None) + # Force a fresh import: pop the submodule from sys.modules AND + # delete the cached attribute on the parent package, otherwise + # `from anthias_server import settings` returns the stale module + # object via the parent's namespace and __init__ never re-runs. + sys.modules.pop('anthias_server.settings', None) + import anthias_server as _anthias_server + if hasattr(_anthias_server, 'settings'): + del _anthias_server.settings try: - import settings + from anthias_server import settings yield (settings, settings.settings) finally: - sys.modules.pop('settings', None) + sys.modules.pop('anthias_server.settings', None) + if hasattr(_anthias_server, 'settings'): + del _anthias_server.settings os.remove(CONFIG_FILE) diff --git a/tests/test_splash_page.py b/tests/test_splash_page.py index 45d46dc7..80402ca9 100644 --- a/tests/test_splash_page.py +++ b/tests/test_splash_page.py @@ -34,7 +34,7 @@ import redis import requests from django.test import Client -from api.views import v2 as v2_views +from anthias_server.api.views import v2 as v2_views _FIXTURE_IPV4 = '192.168.1.42' # NOSONAR _FIXTURE_IPV4_ALT = '10.0.0.5' # NOSONAR @@ -91,7 +91,7 @@ def test_format_drops_garbage_tokens() -> None: # _resolve_node_ip on bare metal (Redis cache fast path) # --------------------------------------------------------------------------- # -# Must never invoke ``lib.utils.get_node_ip()``, which has a ~80s +# Must never invoke ``anthias_common.utils.get_node_ip()``, which has a ~80s # blocking readiness loop that would tail-back every poll. @@ -103,7 +103,7 @@ def bare_metal_no_pending( and an unset ``MY_IP`` env var. ``_resolve_node_ip()`` falls back to ``MY_IP`` on the cache-miss - path (mirroring ``lib.utils.get_node_ip()``); leaving the env var + path (mirroring ``anthias_common.utils.get_node_ip()``); leaving the env var in whatever state the dev shell or CI runner picked up would let that fallback bleed into tests that mean to assert on the no-cache-no-fallback case. Tests that exercise the MY_IP @@ -115,13 +115,13 @@ def bare_metal_no_pending( def test_resolve_reads_from_redis_cache(bare_metal_no_pending: None) -> None: with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object( v2_views.r, 'get', return_value=json.dumps([_FIXTURE_IPV4]) ), mock.patch.object(v2_views.r, 'publish'), mock.patch( - 'api.views.v2.get_node_ip', + 'anthias_server.api.views.v2.get_node_ip', side_effect=AssertionError('must not block on get_node_ip'), ), ): @@ -137,7 +137,7 @@ def test_resolve_cache_hit_also_kicks_off_refresh( — without it, once Redis is populated the cached value would freeze for the rest of the display window.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object( v2_views.r, 'get', return_value=json.dumps([_FIXTURE_IPV4]) ), @@ -153,11 +153,11 @@ def test_resolve_publishes_refresh_on_cache_miss( """Empty cache: return '' and ask host_agent to populate. The next poll picks it up. Don't block waiting for completion.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object(v2_views.r, 'publish') as m_publish, mock.patch( - 'api.views.v2.get_node_ip', + 'anthias_server.api.views.v2.get_node_ip', side_effect=AssertionError('must not block on get_node_ip'), ), ): @@ -171,7 +171,7 @@ def test_resolve_handles_malformed_cache_payload( """Garbage in the cache (e.g. a partial write or a stray byte from another producer) must not crash the resolver.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value='not-valid-json'), mock.patch.object(v2_views.r, 'publish'), ): @@ -186,7 +186,7 @@ def test_resolve_empty_list_in_cache_triggers_refresh( '' without publishing, the splash would never recover when networking comes online during the splash window.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value='[]'), mock.patch.object(v2_views.r, 'publish') as m_publish, ): @@ -201,7 +201,7 @@ def test_resolve_redis_get_failure_returns_empty( The polling endpoint is on a 2s loop — degrading to '' lets the JS keep polling until Redis recovers.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object( v2_views.r, 'get', side_effect=redis.RedisError('synthetic') ), @@ -217,7 +217,7 @@ def test_resolve_debounces_repeat_cache_miss_publishes( tenacity retry). SETNX with TTL gates it: only the first call in the window publishes, later calls within the window no-op.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object(v2_views.r, 'publish') as m_publish, ): @@ -248,7 +248,7 @@ def test_resolve_publish_failure_releases_debounce( assertion that ``_publish_refresh`` invoked it on the right key. """ with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object( v2_views.r, @@ -276,7 +276,7 @@ def test_resolve_setnx_failure_returns_empty( we'd already prevented for get() and publish(). Treat as cache miss and let the JS keep polling.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object( v2_views.r, 'set', side_effect=redis.RedisError('synthetic') @@ -290,7 +290,7 @@ def test_resolve_cache_miss_falls_back_to_my_ip( monkeypatch: pytest.MonkeyPatch, ) -> None: """``bin/upgrade_containers.sh`` exports the host's outbound IP - into the server container as ``MY_IP``. ``lib.utils.get_node_ip()`` + into the server container as ``MY_IP``. ``anthias_common.utils.get_node_ip()`` falls back to it when ``ip_addresses`` is empty in Redis. The polling resolver mirrors that — without the fallback, any setup where host_agent isn't running (custom deploys, late-starting @@ -298,7 +298,7 @@ def test_resolve_cache_miss_falls_back_to_my_ip( 'Detecting network…' forever.""" monkeypatch.setenv('MY_IP', _FIXTURE_IPV4) with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object(v2_views.r, 'publish'), ): @@ -315,7 +315,7 @@ def test_resolve_redis_get_failure_falls_back_to_my_ip( there to cover.""" monkeypatch.setenv('MY_IP', _FIXTURE_IPV4) with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object( v2_views.r, 'get', side_effect=redis.RedisError('synthetic') ), @@ -331,7 +331,7 @@ def test_resolve_cache_miss_with_unset_my_ip_returns_empty( to show 'Detecting network…' until something populates either side).""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object(v2_views.r, 'publish'), ): @@ -348,9 +348,9 @@ def test_resolve_balena_reads_supervisor_response() -> None: fake_response.ok = True fake_response.json.return_value = {'ip_address': _FIXTURE_IPV4} with ( - mock.patch('api.views.v2.is_balena_app', return_value=True), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=True), mock.patch( - 'api.views.v2.get_balena_device_info', return_value=fake_response + 'anthias_server.api.views.v2.get_balena_device_info', return_value=fake_response ) as m_get, ): assert v2_views._resolve_node_ip() == _FIXTURE_IPV4 @@ -363,9 +363,9 @@ def test_resolve_balena_reads_supervisor_response() -> None: def test_resolve_balena_returns_empty_on_supervisor_timeout() -> None: """A slow first-boot supervisor must not block the endpoint.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=True), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=True), mock.patch( - 'api.views.v2.get_balena_device_info', + 'anthias_server.api.views.v2.get_balena_device_info', side_effect=requests.Timeout('synthetic'), ), ): @@ -376,9 +376,9 @@ def test_resolve_balena_returns_empty_on_supervisor_error_status() -> None: fake_response = mock.Mock() fake_response.ok = False with ( - mock.patch('api.views.v2.is_balena_app', return_value=True), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=True), mock.patch( - 'api.views.v2.get_balena_device_info', return_value=fake_response + 'anthias_server.api.views.v2.get_balena_device_info', return_value=fake_response ), ): assert v2_views._resolve_node_ip() == '' @@ -394,7 +394,7 @@ def test_endpoint_returns_200_with_ip_list( bare_metal_no_pending: None, ) -> None: with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object( v2_views.r, 'get', return_value=json.dumps([_FIXTURE_IPV4]) ), @@ -412,7 +412,7 @@ def test_endpoint_returns_200_with_empty_list_on_cache_miss( """Pinned regression: prior code 500'd when get_node_ip() returned 'Unknown'. New code never raises — empty cache → [].""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object(v2_views.r, 'get', return_value=None), mock.patch.object(v2_views.r, 'publish'), ): @@ -428,7 +428,7 @@ def test_endpoint_is_unauthenticated(bare_metal_no_pending: None) -> None: test pins the choice — flipping it to @authorized would silently break the splash on auth-enabled installs.""" with ( - mock.patch('api.views.v2.is_balena_app', return_value=False), + mock.patch('anthias_server.api.views.v2.is_balena_app', return_value=False), mock.patch.object( v2_views.r, 'get', return_value=json.dumps([_FIXTURE_IPV4]) ), @@ -461,7 +461,7 @@ def test_splash_view_does_not_import_get_node_ip() -> None: re-imports it (and might re-introduce the synchronous IP work we just removed) would fail this test before any rendering regression hits production.""" - from anthias_app import views as splash_module + from anthias_server.app import views as splash_module assert not hasattr(splash_module, 'get_node_ip'), ( 'splash_page view should not import get_node_ip; IP resolution ' diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index be160986..a4cf6720 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -7,7 +7,7 @@ import pytest from requests.exceptions import ConnectionError as RequestsConnectionError from requests.exceptions import Timeout -from lib import telemetry +from anthias_server.lib import telemetry @pytest.fixture diff --git a/tests/test_updates.py b/tests/test_updates.py index 4bcaed40..0c7ad905 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -4,7 +4,7 @@ from unittest import mock import pytest -from lib.github import is_up_to_date +from anthias_server.lib.github import is_up_to_date GIT_HASH_1 = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' GIT_SHORT_HASH_1 = 'da39a3e' @@ -15,7 +15,7 @@ logging.disable(logging.CRITICAL) @mock.patch( - 'lib.github.fetch_remote_hash', + 'anthias_server.lib.github.fetch_remote_hash', mock.MagicMock(return_value=(None, False)), ) def test_returns_true_when_git_branch_env_missing() -> None: @@ -98,19 +98,19 @@ def test_is_up_to_date_should_return_value_depending_on_git_hashes( with ( mock.patch( - 'lib.github.fetch_remote_hash', + 'anthias_server.lib.github.fetch_remote_hash', mock.MagicMock(return_value=(latest_remote_hash, False)), ), mock.patch( - 'lib.github.get_git_hash', + 'anthias_server.lib.github.get_git_hash', mock.MagicMock(return_value=git_hash), ), mock.patch( - 'lib.github.get_git_short_hash', + 'anthias_server.lib.github.get_git_short_hash', mock.MagicMock(return_value=git_short_hash), ), mock.patch( - 'lib.github.is_running_latest_published_image', + 'anthias_server.lib.github.is_running_latest_published_image', mock.MagicMock(return_value=published_match), ), ): diff --git a/tests/test_utils.py b/tests/test_utils.py index 5ae797f9..82cea882 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,8 +8,8 @@ import pytest import requests import sh -from lib import utils -from lib.utils import ( +from anthias_common import utils +from anthias_common.utils import ( generate_perfect_paper_password, handler, is_balena_app, @@ -37,7 +37,7 @@ def test_json_tz() -> None: @pytest.mark.django_db def test_url_fails_returns_true_on_connection_error() -> None: with patch( - 'lib.utils.requests.head', + 'anthias_common.utils.requests.head', side_effect=requests.ConnectionError, ): assert url_fails('http://doesnotwork.example.com') is True @@ -47,7 +47,7 @@ def test_url_fails_returns_true_on_connection_error() -> None: def test_url_fails_returns_false_on_2xx_response() -> None: fake = MagicMock() fake.ok = True - with patch('lib.utils.requests.head', return_value=fake): + with patch('anthias_common.utils.requests.head', return_value=fake): assert url_fails('http://example.com') is False @@ -55,14 +55,14 @@ def test_url_fails_returns_false_on_2xx_response() -> None: def test_url_fails_short_circuits_for_invalid_url() -> None: # validate_url() rejects schemeless paths, so url_fails should # return False without ever touching the network layer. - with patch('lib.utils.requests.head') as mock_head: + with patch('anthias_common.utils.requests.head') as mock_head: assert url_fails('/home/user/file') is False mock_head.assert_not_called() @pytest.mark.django_db def test_rtsp_ffprobe_success_returns_false() -> None: - with patch('lib.utils.sh.Command') as mock_command: + with patch('anthias_common.utils.sh.Command') as mock_command: mock_command.return_value.return_value = '' assert not url_fails('rtsp://example.com/stream') mock_command.assert_called_once_with('ffprobe') @@ -71,14 +71,14 @@ def test_rtsp_ffprobe_success_returns_false() -> None: @pytest.mark.django_db def test_rtmp_ffprobe_nonzero_exit_returns_true() -> None: err = sh.ErrorReturnCode_1('ffprobe', b'', b'cannot open stream') - with patch('lib.utils.sh.Command') as mock_command: + with patch('anthias_common.utils.sh.Command') as mock_command: mock_command.return_value.side_effect = err assert url_fails('rtmp://example.com/live') @pytest.mark.django_db def test_rtsp_ffprobe_timeout_returns_true() -> None: - with patch('lib.utils.sh.Command') as mock_command: + with patch('anthias_common.utils.sh.Command') as mock_command: mock_command.return_value.side_effect = sh.TimeoutException( 124, 'ffprobe ...' ) @@ -87,7 +87,7 @@ def test_rtsp_ffprobe_timeout_returns_true() -> None: @pytest.mark.django_db def test_rtsp_ffprobe_missing_returns_false() -> None: - with patch('lib.utils.sh.Command') as mock_command: + with patch('anthias_common.utils.sh.Command') as mock_command: mock_command.side_effect = sh.CommandNotFound('ffprobe') assert not url_fails('rtsp://example.com/stream') @@ -171,9 +171,9 @@ def test_is_demo_node_false(monkeypatch: Any) -> None: def test_is_docker_uses_dockerenv_marker() -> None: - with patch('lib.utils.os.path.isfile', return_value=True): + with patch('anthias_common.utils.os.path.isfile', return_value=True): assert is_docker() is True - with patch('lib.utils.os.path.isfile', return_value=False): + with patch('anthias_common.utils.os.path.isfile', return_value=False): assert is_docker() is False @@ -207,7 +207,7 @@ def test_get_balena_supervisor_api_response_uses_env( monkeypatch.setenv('BALENA_SUPERVISOR_ADDRESS', 'http://supervisor:5000') monkeypatch.setenv('BALENA_SUPERVISOR_API_KEY', 'k') fake = MagicMock() - with patch('lib.utils.requests.get', return_value=fake) as mock_get: + with patch('anthias_common.utils.requests.get', return_value=fake) as mock_get: result = utils.get_balena_supervisor_api_response('get', 'device') assert result is fake url = mock_get.call_args.args[0] @@ -218,7 +218,7 @@ def test_get_balena_device_info_calls_v1_device(monkeypatch: Any) -> None: monkeypatch.setenv('BALENA_SUPERVISOR_ADDRESS', 'http://x') monkeypatch.setenv('BALENA_SUPERVISOR_API_KEY', 'k') fake = MagicMock() - with patch('lib.utils.requests.get', return_value=fake) as mock_get: + with patch('anthias_common.utils.requests.get', return_value=fake) as mock_get: utils.get_balena_device_info() assert '/v1/device' in mock_get.call_args.args[0] @@ -227,7 +227,7 @@ def test_reboot_via_balena_supervisor_uses_post(monkeypatch: Any) -> None: monkeypatch.setenv('BALENA_SUPERVISOR_ADDRESS', 'http://x') monkeypatch.setenv('BALENA_SUPERVISOR_API_KEY', 'k') fake = MagicMock() - with patch('lib.utils.requests.post', return_value=fake) as mock_post: + with patch('anthias_common.utils.requests.post', return_value=fake) as mock_post: utils.reboot_via_balena_supervisor() assert '/v1/reboot' in mock_post.call_args.args[0] @@ -236,7 +236,7 @@ def test_shutdown_via_balena_supervisor_uses_post(monkeypatch: Any) -> None: monkeypatch.setenv('BALENA_SUPERVISOR_ADDRESS', 'http://x') monkeypatch.setenv('BALENA_SUPERVISOR_API_KEY', 'k') fake = MagicMock() - with patch('lib.utils.requests.post', return_value=fake) as mock_post: + with patch('anthias_common.utils.requests.post', return_value=fake) as mock_post: utils.shutdown_via_balena_supervisor() assert '/v1/shutdown' in mock_post.call_args.args[0] @@ -247,7 +247,7 @@ def test_get_balena_supervisor_version_ok(monkeypatch: Any) -> None: fake = MagicMock() fake.ok = True fake.json.return_value = {'version': '14.2.3'} - with patch('lib.utils.requests.get', return_value=fake): + with patch('anthias_common.utils.requests.get', return_value=fake): assert utils.get_balena_supervisor_version() == '14.2.3' @@ -256,7 +256,7 @@ def test_get_balena_supervisor_version_error(monkeypatch: Any) -> None: monkeypatch.setenv('BALENA_SUPERVISOR_API_KEY', 'k') fake = MagicMock() fake.ok = False - with patch('lib.utils.requests.get', return_value=fake): + with patch('anthias_common.utils.requests.get', return_value=fake): assert ( utils.get_balena_supervisor_version() == 'Error getting the Supervisor version' diff --git a/tests/test_viewer.py b/tests/test_viewer.py index 305e7b44..01d17082 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -10,8 +10,8 @@ from unittest import mock import pytest -import viewer -from viewer.scheduling import Scheduler +import anthias_viewer as viewer +from anthias_viewer.scheduling import Scheduler logging.disable(logging.CRITICAL) @@ -78,12 +78,12 @@ def noop(*a: Any, **k: Any) -> None: return None -@mock.patch('viewer.constants.SERVER_WAIT_TIMEOUT', 0) +@mock.patch('anthias_viewer.constants.SERVER_WAIT_TIMEOUT', 0) def test_empty(viewer_fixtures: _ViewerFixtures) -> None: m_asset_list = mock.Mock() m_asset_list.return_value = ([], None) - with mock.patch('viewer.scheduling.generate_asset_list', m_asset_list): + with mock.patch('anthias_viewer.scheduling.generate_asset_list', m_asset_list): setattr(viewer_fixtures.u, 'scheduler', Scheduler()) m_asset_list.assert_called_once() @@ -295,11 +295,11 @@ def test_trigger_recheck_posts_to_recheck_endpoint() -> None: Mocking the token-derivation here keeps this test independent of settings state, which has bitten us under pytest-xdist + Docker test-image conftest configurations.""" - from lib.internal_auth import INTERNAL_AUTH_HEADER + from anthias_common.internal_auth import INTERNAL_AUTH_HEADER with ( - mock.patch('viewer.internal_auth_token', return_value='deadbeef'), - mock.patch('viewer.requests.post') as m, + mock.patch('anthias_viewer.internal_auth_token', return_value='deadbeef'), + mock.patch('anthias_viewer.requests.post') as m, ): viewer._trigger_asset_recheck('abc') m.assert_called_once() @@ -309,7 +309,7 @@ def test_trigger_recheck_posts_to_recheck_endpoint() -> None: def test_trigger_recheck_no_op_on_missing_asset_id() -> None: - with mock.patch('viewer.requests.post') as m: + with mock.patch('anthias_viewer.requests.post') as m: viewer._trigger_asset_recheck(None) m.assert_not_called() @@ -319,8 +319,8 @@ def test_trigger_recheck_no_op_when_internal_token_missing() -> None: in settings or env), the request would be a guaranteed 403 — so the viewer skips it rather than burning an HTTP round-trip.""" with ( - mock.patch('viewer.internal_auth_token', return_value=''), - mock.patch('viewer.requests.post') as m, + mock.patch('anthias_viewer.internal_auth_token', return_value=''), + mock.patch('anthias_viewer.requests.post') as m, ): viewer._trigger_asset_recheck('abc') m.assert_not_called() @@ -332,10 +332,10 @@ def test_trigger_recheck_swallows_request_errors() -> None: with ( mock.patch( - 'viewer.requests.post', + 'anthias_viewer.requests.post', side_effect=_requests.ConnectionError('boom'), ), - mock.patch('viewer.internal_auth_token', return_value='deadbeef'), + mock.patch('anthias_viewer.internal_auth_token', return_value='deadbeef'), ): # Must not raise. viewer._trigger_asset_recheck('abc') @@ -355,8 +355,8 @@ def test_asset_loop_does_not_recheck_missing_local_asset() -> None: skip_event = mock.Mock() skip_event.wait.return_value = False with ( - mock.patch('viewer._trigger_asset_recheck') as trigger, - mock.patch('viewer.get_skip_event', return_value=skip_event), + mock.patch('anthias_viewer._trigger_asset_recheck') as trigger, + mock.patch('anthias_viewer.get_skip_event', return_value=skip_event), ): viewer.asset_loop(scheduler) trigger.assert_not_called() @@ -376,8 +376,8 @@ def test_asset_loop_rechecks_unreachable_remote_asset() -> None: skip_event = mock.Mock() skip_event.wait.return_value = False with ( - mock.patch('viewer._trigger_asset_recheck') as trigger, - mock.patch('viewer.get_skip_event', return_value=skip_event), + mock.patch('anthias_viewer._trigger_asset_recheck') as trigger, + mock.patch('anthias_viewer.get_skip_event', return_value=skip_event), ): viewer.asset_loop(scheduler) trigger.assert_called_once_with('remote') diff --git a/tests/test_views_files.py b/tests/test_views_files.py index 62cee080..40ffd690 100644 --- a/tests/test_views_files.py +++ b/tests/test_views_files.py @@ -9,8 +9,8 @@ import pytest from django.http import Http404, HttpRequest, HttpResponseBase from django.test import RequestFactory -from anthias_app import views_files -from anthias_app.views_files import ( +from anthias_server.app import views_files +from anthias_server.app.views_files import ( DOCKER_BRIDGE_CIDR, RFC1918_CIDRS, _client_ip, diff --git a/raspberry_pi_imager/README.md b/tools/raspberry_pi_imager/README.md similarity index 100% rename from raspberry_pi_imager/README.md rename to tools/raspberry_pi_imager/README.md diff --git a/raspberry_pi_imager/tests/__init__.py b/tools/raspberry_pi_imager/__init__.py similarity index 100% rename from raspberry_pi_imager/tests/__init__.py rename to tools/raspberry_pi_imager/__init__.py diff --git a/tools/raspberry_pi_imager/bin/build-pi-imager-json.py b/tools/raspberry_pi_imager/bin/build-pi-imager-json.py new file mode 100755 index 00000000..6597006e --- /dev/null +++ b/tools/raspberry_pi_imager/bin/build-pi-imager-json.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from tools.raspberry_pi_imager.build_pi_imager_json import main + +if __name__ == '__main__': + main() diff --git a/raspberry_pi_imager/build_pi_imager_json.py b/tools/raspberry_pi_imager/build_pi_imager_json.py similarity index 100% rename from raspberry_pi_imager/build_pi_imager_json.py rename to tools/raspberry_pi_imager/build_pi_imager_json.py diff --git a/tools/raspberry_pi_imager/tests/__init__.py b/tools/raspberry_pi_imager/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/raspberry_pi_imager/tests/test_build_pi_imager_json.py b/tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py similarity index 98% rename from raspberry_pi_imager/tests/test_build_pi_imager_json.py rename to tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py index 6c526db1..a4b7dae8 100644 --- a/raspberry_pi_imager/tests/test_build_pi_imager_json.py +++ b/tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest -from raspberry_pi_imager.build_pi_imager_json import ( +from tools.raspberry_pi_imager.build_pi_imager_json import ( MAINTENANCE_SUFFIX, REQUIRED_FIELDS, SUPPORTED_BOARDS, @@ -86,7 +86,7 @@ def mock_requests_get() -> Iterator[MagicMock]: """Patches the module-level requests.get and yields the mock so each test can configure return_value or side_effect.""" with patch( - 'raspberry_pi_imager.build_pi_imager_json.requests.get' + 'tools.raspberry_pi_imager.build_pi_imager_json.requests.get' ) as mock_get: yield mock_get diff --git a/uv.lock b/uv.lock index dd7f2b1b..8eb0ab20 100644 --- a/uv.lock +++ b/uv.lock @@ -90,7 +90,7 @@ wheels = [ [[package]] name = "anthias" version = "0.20.4" -source = { virtual = "." } +source = { editable = "." } [package.dev-dependencies] dev = [