diff --git a/.copier-answers.resonant.yml b/.copier-answers.resonant.yml
index e4bb3e43..408540a9 100644
--- a/.copier-answers.resonant.yml
+++ b/.copier-answers.resonant.yml
@@ -1,4 +1,4 @@
-_commit: v0.48.1
+_commit: v0.50.3
_src_path: https://github.com/kitware-resonant/cookiecutter-resonant
core_app_name: core
include_example_code: false
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..1a9382e2
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,93 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
+{
+ "name": "bats-ai",
+ "dockerComposeFile": [
+ "../docker-compose.yml",
+ "../docker-compose.override.yml",
+ "./docker-compose.devcontainer.yml"
+ ],
+ "service": "django",
+ "overrideCommand": true,
+ // The "vscode" user and remoteUser are set by the base image label (devcontainers/base).
+ "workspaceFolder": "/home/vscode/bats-ai",
+ "features": {
+ "ghcr.io/devcontainers/features/git-lfs:1": {},
+ "ghcr.io/devcontainers/features/node:1": {},
+ "ghcr.io/rails/devcontainer/features/postgres-client:1": {
+ "version": 18
+ },
+ "ghcr.io/devcontainers/features/terraform:1": {},
+ "ghcr.io/devcontainers/features/aws-cli:1": {},
+ "ghcr.io/devcontainers/features/github-cli:1": {},
+ "ghcr.io/devcontainers-extra/features/heroku-cli:1": {}
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ // Python
+ "ms-python.python",
+ "ms-python.vscode-pylance",
+ "ms-python.debugpy",
+ "ms-python.mypy-type-checker",
+ "charliermarsh.ruff",
+ // Django
+ "batisteo.vscode-django",
+ "augustocdias.tasks-shell-input",
+ // Other file formats
+ "editorconfig.editorconfig",
+ "mikestead.dotenv",
+ "tamasfe.even-better-toml",
+ "timonwong.shellcheck",
+ // Infrastructure
+ "ms-azuretools.vscode-containers",
+ "hashicorp.terraform",
+ "github.vscode-github-actions",
+ // Remove AWS extension, as only the CLI is wanted; see: https://github.com/devcontainers/features/issues/1228
+ "-AmazonWebServices.aws-toolkit-vscode"
+ ],
+ "settings": {
+ "containers.containerClient": "com.microsoft.visualstudio.containers.docker",
+ // Container-specific Python paths
+ "python.defaultInterpreterPath": "/home/vscode/venv/bin/python",
+ // Ensure that `envFile` from any user settings is ignored; Docker Compose provides it.
+ "python.envFile": "",
+ // Reduce file watcher overhead for generated/cache directories.
+ "files.watcherExclude": {
+ "**/__pycache__/**": true,
+ "**/.pytest_cache/**": true,
+ "**/node_modules/**": true
+ }
+ }
+ }
+ },
+ // Prevent a prompt every time the debugger opens a port or Django auto-restarts.
+ "otherPortsAttributes": {
+ "onAutoForward": "silent"
+ },
+ "portsAttributes": {
+ "8000": {
+ "label": "Django",
+ // Show a dialog if the port isn't free.
+ "requireLocalPort": true,
+ "onAutoForward": "silent"
+ },
+ "8080": {
+ "label": "Vite",
+ "requireLocalPort": true,
+ "onAutoForward": "silent"
+ }
+ },
+ // Install a global Python and create a venv before VSCode extensions start,
+ // to prevent prompts and ensure test discovery works on first load.
+ "onCreateCommand": {
+ "python": ["uv", "python", "install", "--default"],
+ "venv": ["uv", "sync", "--all-extras", "--all-groups"],
+ "npm": ["npm", "--prefix", "client", "install"]
+ },
+ // Ensure it is re-synced on restarts.
+ "updateContentCommand": {
+ "venv": ["uv", "sync", "--all-extras", "--all-groups"],
+ "npm": ["npm", "--prefix", "client", "install"]
+ }
+}
diff --git a/.devcontainer/docker-compose.devcontainer.yml b/.devcontainer/docker-compose.devcontainer.yml
new file mode 100644
index 00000000..3189ee29
--- /dev/null
+++ b/.devcontainer/docker-compose.devcontainer.yml
@@ -0,0 +1,14 @@
+services:
+ django:
+ # Don't expose ports, devcontainer forwarding is superior, since we can just bind to localhost.
+ ports: !reset []
+ # Don't auto-run the default command, launch.json or the terminal will be used.
+ command: !reset []
+
+ celery:
+ # Celery will be started via launch.json or the terminal.
+ profiles: ["celery"]
+
+ client:
+ # npm will be started via launch.json or the terminal.
+ profiles: ["client"]
diff --git a/.editorconfig b/.editorconfig
index 781569b5..e6c9576e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -27,12 +27,11 @@ indent_size = 2
indent_size = 4
max_line_length = 100
-[*.toml]
+[*.sh]
indent_size = 2
-[*.{yml,yaml}]
+[*.toml]
indent_size = 2
-[*.{js,jsx,ts,tsx,vue}]
+[*.{yml,yaml}]
indent_size = 2
-max_line_length = 100
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yml
similarity index 89%
rename from .github/workflows/ci.yaml
rename to .github/workflows/ci.yml
index f6978f74..3e4ddd99 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yml
@@ -1,11 +1,9 @@
name: CI
on:
pull_request:
- # types: [opened, synchronize]
- # TODO: why is this here?
push:
branches:
- - "main"
+ - main
permissions:
contents: read
jobs:
@@ -15,7 +13,7 @@ jobs:
matrix:
linter: [eslint, typescript]
name: Lint [${{ matrix.linter }}]
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -32,7 +30,7 @@ jobs:
working-directory: client
test-python:
name: Test Python
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
services:
postgres:
image: postgis/postgis:latest
@@ -55,7 +53,7 @@ jobs:
- 5672:5672
minio:
# This image does not require any command arguments (which GitHub Actions don't support)
- image: bitnamilegacy/minio:2025.7.23
+ image: bitnamilegacy/minio:latest
env:
MINIO_ROOT_USER: minioAccessKey
MINIO_ROOT_PASSWORD: minioSecretKey
@@ -66,16 +64,15 @@ jobs:
--health-start-interval 2s
ports:
- 9000:9000
-
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
-
- name: Run tests
- run: uv run tox
+ run: |
+ uv run tox
env:
DJANGO_DATABASE_URL: postgres://postgres:postgres@localhost:5432/django
DJANGO_CELERY_BROKER_URL: amqp://localhost:5672/
- DJANGO_MINIO_STORAGE_URL: http://minioAccessKey:minioSecretKey@localhost:9000/django-storage-testing
+ DJANGO_MINIO_STORAGE_URL: http://minioAccessKey:minioSecretKey@localhost:9000/django-storage
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..c1357687
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,95 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Django: Server",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/manage.py",
+ "args": ["runserver_plus", "--print-sql", "localhost:8000"],
+ "django": true,
+ "console": "integratedTerminal"
+ },
+ {
+ "name": "Django: Server (eager Celery)",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/manage.py",
+ "args": ["runserver_plus", "--print-sql", "localhost:8000"],
+ "env": {
+ "DJANGO_CELERY_TASK_ALWAYS_EAGER": "true"
+ },
+ "django": true,
+ "console": "integratedTerminal"
+ },
+ {
+ "name": "Django: Management Command",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/manage.py",
+ "args": ["${input:managementCommand}"],
+ "django": true,
+ "console": "integratedTerminal"
+ },
+ {
+ "name": "Celery: Worker",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "celery",
+ "args": [
+ "--app",
+ "bats_ai.celery",
+ "worker",
+ "--loglevel",
+ "INFO",
+ "--pool",
+ "solo",
+ "--without-heartbeat"
+ ],
+ "console": "integratedTerminal",
+ "justMyCode": false
+ },
+ {
+ "name": "Pytest: Debug",
+ "type": "debugpy",
+ "request": "launch",
+ "purpose": ["debug-test"],
+ "console": "integratedTerminal",
+ "django": true,
+ "justMyCode": false
+ },
+ {
+ "name": "Vite: Dev Server",
+ "type": "node",
+ "request": "launch",
+ "cwd": "${workspaceFolder}/client",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": ["run", "dev"],
+ "console": "integratedTerminal"
+ }
+ ],
+ "compounds": [
+ {
+ "name": "Django + Celery",
+ "configurations": ["Django: Server", "Celery: Worker"],
+ "stopAll": true
+ },
+ {
+ "name": "Django + Celery + Vite",
+ "configurations": ["Django: Server", "Celery: Worker", "Vite: Dev Server"],
+ "stopAll": true
+ }
+ ],
+ "inputs": [
+ {
+ "id": "managementCommand",
+ "type": "command",
+ "command": "shellCommand.execute",
+ "args": {
+ "command": "./manage.py help --commands",
+ "description": "Django management command",
+ "allowCustomValues": true
+ }
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..90cc42f9
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,37 @@
+{
+ // File cleanup
+ "files.trimTrailingWhitespace": true,
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+
+ // Python
+ "python.analysis.autoFormatStrings": true,
+ "python.testing.pytestEnabled": true,
+ "python.analysis.autoImportCompletions": true,
+ "python.analysis.gotoDefinitionInStringLiteral": true,
+ // Allow auto-importing from deeper symbols inside of Django.
+ "python.analysis.packageIndexDepths": [
+ {
+ "name": "django",
+ "depth": 6
+ }
+ ],
+ "python.analysis.inlayHints.pytestParameters": true,
+
+ // Django templates
+ "emmet.includeLanguages": {
+ "django-html": "html"
+ },
+
+ // Type checking: Use Mypy and disable Pylance.
+ "mypy-type-checker.importStrategy": "fromEnvironment",
+ // Mypy daemon seems better, but is buggy in practice.
+ "mypy-type-checker.preferDaemon": false,
+ "mypy-type-checker.reportingScope": "file",
+ "python.analysis.typeCheckingMode": "off",
+
+ // Ruff
+ "[python]": {
+ "editor.defaultFormatter": "charliermarsh.ruff"
+ }
+}
diff --git a/README.md b/README.md
index 9b9b1969..e8f5cc34 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,51 @@
# bats-ai
-## Develop with Docker (recommended quickstart)
-
-This is the simplest configuration for developers to start with.
-
-### Initial Setup
-
-1. Run `docker compose run --rm django ./manage.py migrate`
-2. Run `docker compose run --rm django ./manage.py createsuperuser`
- and follow the prompts to create your own user
-3. Run `docker compose run --rm django ./manage.py loaddata species` to load species
- data into the database
-
-### Run Vue Frontend
-
-1. Run `cd client/`
-2. Run `npm install`
-3. Run `npm run dev`
-
-### Run Application
-
-1. Run `docker compose up`
-2. Access the site, starting at
-3. When finished, use `Ctrl+C`
-
-### Maintenance
-
-To non-destructively update your development stack at any time:
-
-1. Run `docker compose down`
-2. Run `docker compose pull`
-3. Run `docker compose build --pull`
-4. Run `docker compose run --rm django ./manage.py migrate`
+## Setup
+1. Install [VS Code with dev container support](https://code.visualstudio.com/docs/devcontainers/containers#_installation).
+1. Open the project in VS Code, then run `Dev Containers: Reopen in Container`
+ from the Command Palette (`Ctrl+Shift+P`).
+1. Once the container is ready, open a terminal and run:
+ ```sh
+ ./manage.py migrate
+ ./manage.py createsuperuser
+ ./manage.py loaddata species
+ ```
+
+## Run
+Open the **Run and Debug** panel (`Ctrl+Shift+D`) and select a launch configuration:
+
+* **Django: Server** — Starts the development server at
+* **Django: Server (eager Celery)** — Same, but Celery tasks run synchronously
+ in the web process (useful for debugging task code without a worker)
+* **Celery: Worker** — Starts only the Celery worker
+* **Django + Celery** — Starts both the server and a Celery worker
+* **Django: Management Command** — Pick and run any management command
+* **Vite: Dev Server** - Starts the frontend development server at
+* **Django + Celery + Vite** - Starts the server, a Celery worker, and the frontend.
+
+## Test
+Run the full test suite from a terminal: `tox`
+
+Auto-format code: `tox -e format`
+
+Run and debug individual tests from the **Testing** panel (`Ctrl+Shift+;`).
+
+## Rebuild
+After changes to the Dockerfile, Docker Compose files, or `devcontainer.json`,
+run `Dev Containers: Rebuild Container` from the Command Palette (`Ctrl+Shift+P`).
+
+For dependency changes in `pyproject.toml`, just run `uv sync --all-extras --all-groups`.
## Dev Tool Endpoints
-1. Main Site Interface [http://localhost:8080/](http://localhost:8080/)
-2. Site Administration [http://localhost:8000/admin/](http://localhost:8000/admin/)
-3. Swagger API (These are default swagger endpoints using Django-REST) [http://localhost:8000/api/docs/swagger/](http://localhost:8000/api/docs/swagger/)
-4. Django Ninja API [http://localhost:8000/api/v1/docs#/](http://localhost:8000/api/v1/docs#/)
-5. MinIO (S3 local management) [http://localhost:9001/browser](http://localhost:9001/browser)
+1. Main Site Interface
+2. Site Administration
+3. Swagger API (These are default swagger endpoints using Django-REST)
+4. Django Ninja API
+5. MinIO (S3 local management)
Username: 'minioAccessKey'
Password: 'minioSecretKey'
-## Develop Natively (advanced)
-
-This configuration still uses Docker to run attached services in the background,
-but allows developers to run Python code on their native system.
-
-### Initial Setup for Native Development
-
-1. Run `docker compose -f ./docker-compose.yml up -d`
-2. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/)
-3. Run `export UV_ENV_FILE=./dev/.env.docker-compose-native`
-4. Run `./manage.py migrate`
-5. Run `./manage.py createsuperuser` and follow the prompts to create your own user
-
-### Run Native Application
-
-1. Ensure `docker compose -f ./docker-compose.yml up -d` is still active
-2. Run `export UV_ENV_FILE=./dev/.env.docker-compose-native`
-3. Run: `./manage.py runserver_plus`
-4. Run in a separate terminal: `uv run celery --app bats_ai.celery worker --loglevel INFO --without-heartbeat`
-5. Run in a separate terminal:
- 1. `source ./dev/export-env.sh`
- 2. `cd ./client`
- 3. `npm install`
- 4. `npm run dev`
-6. When finished, run `docker compose stop`
-
## Importing Recordings
The `importRecordings` management command allows you to bulk import WAV files from a
@@ -84,14 +61,14 @@ directory. It will:
**Basic usage with Docker Compose (with bind mount):**
-```bash
-docker compose run --rm -v /path/to/wav/files:/data django ./manage.py importRecordings /data
+```sh
+./manage.py importRecordings /data
```
**With options:**
```bash
-docker compose run --rm -v /path/to/wav/files:/data django ./manage.py importRecordings /data \
+./manage.py importRecordings /data \
--owner username \
--public \
--limit 10
@@ -104,47 +81,6 @@ docker compose run --rm -v /path/to/wav/files:/data django ./manage.py importRec
- `--public`: Make imported recordings public
- `--limit N`: Limit the number of WAV files to import (useful for testing)
-**Example with bind mount:**
-
-```bash
-docker compose run --rm \
- -v /media/bryon.lewis/Elements/BATSAI/training_files:/data \
- django ./manage.py importRecordings /data --limit 5
-```
-
-This will:
-
-1. Mount your host directory `/media/bryon.lewis/Elements/BATSAI/training_files` to `/data` in the container
-2. Import only the first 5 WAV files found
-3. Use the first superuser as the owner
-4. Create private recordings (unless `--public` is specified)
-
-## Testing
-
-### Initial Setup for Testing
-
-tox is used to manage the execution of all tests.
-[Install `uv`](https://docs.astral.sh/uv/getting-started/installation/) and run tox with
-`uv run tox ...`.
-
-When running the "Develop with Docker" configuration, all tox commands must be run as
-`docker compose run --rm django uv run tox`; extra arguments may also be appended to this form.
-
-### Running Tests
-
-Run `uv run tox` to launch the full test suite.
-
-Individual test environments may be selectively run.
-This also allows additional options to be be added.
-Useful sub-commands include:
-
-- `uv run tox -e lint`: Run only the style checks
-- `uv run tox -e type`: Run only the type checks
-- `uv run tox -e test`: Run only the pytest-driven tests
-
-To automatically reformat all code to comply with
-some (but not all) of the style checks, run `uv run tox -e format`.
-
## Code Formatting
It's recommended that you use `pre-commit` to provide additional
@@ -186,7 +122,7 @@ is preferred because once the commit is created locally, you will need to rebase
or otherwise rewrite the commit to make adjustments if done after the fact.
Lastly, the GitLab CI/CD infrastructure runs the same `pre-commit` configuration
-on all pipelines for new MRs. The automated checks in GitLab are optional, but
+on all pipelines for new MRs. The automated checks in GitLab are optional, but
it is highly recommended to perform these checks locally prior to pushing new
commits.
diff --git a/bats_ai/core/views/export_annotation.py b/bats_ai/core/views/export_annotation.py
index dece2fbe..2ebd0487 100644
--- a/bats_ai/core/views/export_annotation.py
+++ b/bats_ai/core/views/export_annotation.py
@@ -1,7 +1,7 @@
from __future__ import annotations
+from datetime import datetime
import logging
-from typing import TYPE_CHECKING
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
@@ -10,9 +10,6 @@
from bats_ai.core.models import ExportedAnnotationFile
-if TYPE_CHECKING:
- from datetime import datetime
-
logger = logging.getLogger(__name__)
router = Router()
diff --git a/dev/django.Dockerfile b/dev/django.Dockerfile
index d94c6b59..f62c265c 100644
--- a/dev/django.Dockerfile
+++ b/dev/django.Dockerfile
@@ -1,13 +1,33 @@
-FROM ghcr.io/astral-sh/uv:debian
-
-# Make Python more friendly to running in containers
-ENV PYTHONDONTWRITEBYTECODE=1 \
- PYTHONUNBUFFERED=1
-
-# Make uv install content in well-known locations
-ENV UV_PROJECT_ENVIRONMENT=/var/lib/venv \
- UV_CACHE_DIR=/var/cache/uv/cache \
- UV_PYTHON_INSTALL_DIR=/var/cache/uv/bin \
- # The uv cache and environment are expected to be mounted on different volumes,
- # so hardlinks won't work
- UV_LINK_MODE=symlink
+FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
+
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
+
+# Ensure Python output appears immediately in container logs.
+ENV PYTHONUNBUFFERED=1
+
+# Put the uv and npm caches in a separate location,
+# where they can persist and be shared across containers.
+# The uv cache and virtual environment are on different volumes, so hardlinks won't work.
+ENV UV_CACHE_DIR=/home/vscode/pkg-cache/uv \
+ UV_PYTHON_INSTALL_DIR=/home/vscode/pkg-cache/uv-python \
+ UV_LINK_MODE=symlink \
+ NPM_CONFIG_CACHE=/home/vscode/pkg-cache/npm
+
+# Put the virtual environment outside the project directory,
+# to improve performance on macOS and prevent accidental usage from the host machine.
+# Activate it, so `uv run` doesn't need to be prefixed.
+ENV UV_PROJECT_ENVIRONMENT=/home/vscode/venv \
+ PATH="/home/vscode/venv/bin:$PATH"
+
+# Put tool scratch files outside the project directory too.
+ENV TOX_WORK_DIR=/home/vscode/tox \
+ RUFF_CACHE_DIR=/home/vscode/.cache/ruff \
+ MYPY_CACHE_DIR=/home/vscode/.cache/mypy
+
+RUN ["chsh", "-s", "/usr/bin/zsh", "vscode"]
+
+USER vscode
+
+# Pre-create named volume mount points, so the new volume inherits `vscode` user ownership:
+# https://docs.docker.com/engine/storage/volumes/#populate-a-volume-using-a-container
+RUN ["mkdir", "/home/vscode/pkg-cache"]
diff --git a/dev/docker-development.md b/dev/docker-development.md
new file mode 100644
index 00000000..a5faba3c
--- /dev/null
+++ b/dev/docker-development.md
@@ -0,0 +1,23 @@
+# Docker Compose Development (without VS Code)
+
+An alternative to the recommended [dev container](../README.md) workflow.
+
+## Setup
+1. `docker compose run --rm django ./manage.py migrate`
+1. `docker compose run --rm django ./manage.py createsuperuser`
+
+## Run
+1. `docker compose up`
+1. Access http://localhost:8000/
+1. `Ctrl+C` to stop
+
+To include the Celery worker: `docker compose --profile celery up`
+
+## Update
+1. `docker compose down`
+1. `docker compose pull`
+1. `docker compose build --pull`
+1. `docker compose run --rm django ./manage.py migrate`
+
+## Reset
+Remove all data and volumes: `docker compose down -v`
diff --git a/dev/export-env.sh b/dev/export-env.sh
index 6ffbebaf..4ea1678e 100644
--- a/dev/export-env.sh
+++ b/dev/export-env.sh
@@ -1,26 +1,28 @@
+# shellcheck shell=bash
# Export environment variables from the .env file in the first argument.
# If no argument is given, default to "dev/.env.docker-compose-native".
# This file must be sourced, not run.
if [ -n "$1" ]; then
- # If an argument was provided, use it as the .env file
- _dotenv_file="$1"
+ # If an argument was provided, use it as the .env file
+ _dotenv_file="$1"
else
- # Otherwise, use the default .env file
- if [ -n "$ZSH_VERSION" ]; then
- # ZSH has a different way to get the directory of the current script
- _dotenv_dir="$0:A:h"
- else
- # Assume this is Bash
- _dotenv_dir="$( dirname "${BASH_SOURCE[0]}" )"
- fi
- _dotenv_file="${_dotenv_dir}/.env.docker-compose-native"
+ # Otherwise, use the default .env file
+ if [ -n "$ZSH_VERSION" ]; then
+ # ZSH has a different way to get the directory of the current script
+ _dotenv_dir="$0:A:h"
+ else
+ # Assume this is Bash
+ _dotenv_dir="$( dirname "${BASH_SOURCE[0]}" )"
+ fi
+ _dotenv_file="${_dotenv_dir}/.env.docker-compose-native"
fi
# Export all assignments in the $_dotenv_file
# Using "set -a" allows .env files with spaces or comments to work seamlessly
# https://stackoverflow.com/a/45971167
set -a
+# shellcheck source=.env.docker-compose-native
. "$_dotenv_file"
set +a
diff --git a/dev/native-development.md b/dev/native-development.md
new file mode 100644
index 00000000..4f122bb7
--- /dev/null
+++ b/dev/native-development.md
@@ -0,0 +1,19 @@
+# Native Development (advanced)
+
+Runs Python on the host while using Docker Compose for services.
+
+## Setup
+1. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/)
+1. Start services: `docker compose -f ./docker-compose.yml up -d`
+1. Load environment: `source ./dev/export-env.sh`
+1. `./manage.py migrate`
+1. `./manage.py createsuperuser`
+
+## Run
+1. Ensure services are running: `docker compose -f ./docker-compose.yml up -d`
+1. `source ./dev/export-env.sh`
+1. `./manage.py runserver_plus`
+1. In a separate terminal: `celery --app bats_ai.celery worker --loglevel INFO --without-heartbeat`
+1. Access http://localhost:8000/
+
+Stop services when done: `docker compose stop`
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 54d2721c..261610f7 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -9,12 +9,11 @@ services:
]
# Log printing is enhanced by a TTY
tty: true
- environment:
- UV_ENV_FILE: ./dev/.env.docker-compose
- working_dir: /opt/django-project
+ working_dir: /home/vscode/bats-ai
+ env_file: ./dev/.env.docker-compose
volumes:
- - .:/opt/django-project
- - uv_cache:/var/cache/uv
+ - .:/home/vscode/bats-ai
+ - pkg_cache:/home/vscode/pkg-cache
ports:
- 8000:8000
depends_on:
@@ -38,14 +37,13 @@ services:
"--loglevel", "INFO",
"--without-heartbeat"
]
- # Docker Compose does not set the TTY width, which causes Celery errors
+ # uv progress doesn't display properly with a Docker TTY
tty: false
- environment:
- UV_ENV_FILE: ./dev/.env.docker-compose
- working_dir: /opt/django-project
+ working_dir: /home/vscode/bats-ai
+ env_file: ./dev/.env.docker-compose
volumes:
- - .:/opt/django-project
- - uv_cache:/var/cache/uv
+ - .:/home/vscode/bats-ai
+ - pkg_cache:/home/vscode/pkg-cache
depends_on:
postgres:
condition: service_healthy
@@ -71,4 +69,4 @@ services:
- ./dev/.env.docker-compose
volumes:
- uv_cache:
+ pkg_cache:
diff --git a/pyproject.toml b/pyproject.toml
index 838d780c..b7e76c9b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ dependencies = [
"django-environ==0.13.0",
"django-extensions==4.1",
"django-oauth-toolkit==3.2.0",
- "django-resonant-settings[allauth,celery]==0.48.1",
+ "django-resonant-settings[allauth,celery]==0.50.3",
"django-resonant-utils[allauth,s3_storage]==0.19.0",
"django-s3-file-field[s3]==1.1.0",
"django-storages[s3]==1.14.6",
@@ -207,6 +207,10 @@ extend-immutable-calls = ["ninja.Query"]
[tool.ruff.lint.flake8-self]
extend-ignore-names = ["_base_manager", "_default_manager", "_meta"]
+[tool.ruff.lint.flake8-type-checking]
+runtime-evaluated-base-classes = ["pydantic.BaseModel"]
+runtime-evaluated-decorators = ["pydantic.validate_call"]
+
[tool.ruff.lint.isort]
# Sort by name, don't cluster "from" vs "import"
force-sort-within-sections = true
diff --git a/tox.ini b/tox.ini
index b1108c43..9fedc806 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,6 +12,8 @@ env_list =
runner = uv-venv-lock-runner
pass_env =
DJANGO_*
+ RUFF_CACHE_DIR
+ MYPY_CACHE_DIR
extras =
development
diff --git a/uv.lock b/uv.lock
index bf6155c5..fe0cc974 100644
--- a/uv.lock
+++ b/uv.lock
@@ -305,7 +305,7 @@ requires-dist = [
{ name = "django-minio-storage", marker = "extra == 'development'", specifier = "==0.5.9" },
{ name = "django-ninja", specifier = "==1.6.2" },
{ name = "django-oauth-toolkit", specifier = "==3.2.0" },
- { name = "django-resonant-settings", extras = ["allauth", "celery"], specifier = "==0.48.1" },
+ { name = "django-resonant-settings", extras = ["allauth", "celery"], specifier = "==0.50.3" },
{ name = "django-resonant-utils", extras = ["allauth", "s3-storage"], specifier = "==0.19.0" },
{ name = "django-resonant-utils", extras = ["minio-storage"], marker = "extra == 'development'", specifier = "==0.19.0" },
{ name = "django-s3-file-field", extras = ["minio"], marker = "extra == 'development'", specifier = "==1.1.0" },
@@ -949,15 +949,15 @@ wheels = [
[[package]]
name = "django-resonant-settings"
-version = "0.48.1"
+version = "0.50.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-environ" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/93/36/70d9e2441a204e138c50c97b7791efa8e8bc15e17dfea2c2413733d2e576/django_resonant_settings-0.48.1.tar.gz", hash = "sha256:c101ccc60e6922e875140eeb3c2f089aac2bf0ebca63c512f826f91286d16983", size = 19231, upload-time = "2026-03-06T05:01:10.797Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/93/09/90d8a1bf0adc369da51f94ad001ff0ac0a66021ab1ea33b3732d80cb2e78/django_resonant_settings-0.50.3.tar.gz", hash = "sha256:a961cba3fe850845d636caccf3b927068cc25884a14f28d7e751bb16f0a8d9be", size = 19236, upload-time = "2026-04-10T14:04:25.793Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5e/f2/15b8afe28ed7b76e8e7f7d6bac8b49cc977691142659d5468671c0889536/django_resonant_settings-0.48.1-py3-none-any.whl", hash = "sha256:01e494132d05828c4a79b54d1e5013052b4e40cc034e7dd52cf8b4c93a57e4e7", size = 26102, upload-time = "2026-03-06T05:01:09.754Z" },
+ { url = "https://files.pythonhosted.org/packages/be/a5/b311d7038abf599098a6dcc7652fb26969cf744fef7eca28e82b8b94b5c0/django_resonant_settings-0.50.3-py3-none-any.whl", hash = "sha256:ada78d0cd0995596510eeb43e18fc278250f9d7962688b34515caceb39e72fb7", size = 26047, upload-time = "2026-04-10T14:04:26.541Z" },
]
[package.optional-dependencies]