Merge pull request #9 from ipnet-mesh/claude/add-data-home-config-01WnK1Q9vczq5atX7U5P3yJz

Add configurable data directory for services
This commit is contained in:
JingleManSweep
2025-12-03 19:47:49 +00:00
committed by GitHub
10 changed files with 363 additions and 76 deletions
+21 -4
View File
@@ -11,6 +11,23 @@
# MESHCORE_IMAGE=ghcr.io/ipnet-mesh/meshcore-hub:v1.0.0
MESHCORE_IMAGE=
# ===================
# Data Directory
# ===================
# Base directory for all service data (collector DB, tags, members, etc.)
# Default: ./data (relative to docker-compose.yml location)
# Inside containers this is mapped to /data
#
# Structure:
# ${DATA_HOME}/
# ├── collector/
# │ ├── meshcore.db # SQLite database
# │ └── tags.json # Node tags for import
# └── web/
# └── members.json # Network members list
DATA_HOME=./data
# ===================
# Common Settings
# ===================
@@ -72,10 +89,10 @@ NETWORK_RADIO_CONFIG=
NETWORK_CONTACT_EMAIL=
NETWORK_CONTACT_DISCORD=
# Path to members JSON file (mounted into container)
# For production: use ./data/members.json
# For testing with example data: use ./example/data/members.json
MEMBERS_FILE_PATH=./example/data/members.json
# Members file location (optional override)
# Default: ${DATA_HOME}/web/members.json
# Only set this if you want to use a different location
# MEMBERS_FILE=/custom/path/to/members.json
# ===================
# Webhook Settings
+22 -4
View File
@@ -283,9 +283,12 @@ meshcore-hub/
│ │ └── tags.json # Example node tags data
│ └── web/
│ └── members.json # Example network members data
├── data/ # Runtime data (gitignored)
│ ├── collector/ # Collector data (tags.json)
└── web/ # Web data (members.json)
├── data/ # Runtime data (gitignored, DATA_HOME default)
│ ├── collector/ # Collector data
│ ├── meshcore.db # SQLite database
│ │ └── tags.json # Node tags for import
│ └── web/ # Web data
│ └── members.json # Network members list
├── Dockerfile # Docker build configuration
├── docker-compose.yml # Docker Compose services (gitignored)
└── docker-compose.yml.example # Docker Compose template
@@ -433,11 +436,26 @@ meshcore-hub interface --mode receiver --mock
See [PLAN.md](PLAN.md#configuration-environment-variables) for complete list.
Key variables:
- `DATA_HOME` - Base directory for all service data (default: `./data`)
- `MQTT_HOST`, `MQTT_PORT`, `MQTT_PREFIX` - MQTT broker connection
- `DATABASE_URL` - SQLAlchemy database URL
- `DATABASE_URL` - SQLAlchemy database URL (default: `sqlite:///{DATA_HOME}/collector/meshcore.db`)
- `API_READ_KEY`, `API_ADMIN_KEY` - API authentication keys
- `LOG_LEVEL` - Logging verbosity
### Data Directory Structure
The `DATA_HOME` environment variable controls where all service data is stored:
```
${DATA_HOME}/
├── collector/
│ ├── meshcore.db # SQLite database
│ └── tags.json # Node tags for import
└── web/
└── members.json # Network members list
```
Services automatically create their subdirectories if they don't exist.
### Webhook Configuration
The collector supports forwarding events to external HTTP endpoints:
+16 -3
View File
@@ -456,6 +456,7 @@ meshcore/+/command/request_telemetry
### Common
| Variable | Default | Description |
|----------|---------|-------------|
| DATA_HOME | ./data | Base directory for service data |
| LOG_LEVEL | INFO | Logging level |
| MQTT_HOST | localhost | MQTT broker host |
| MQTT_PORT | 1883 | MQTT broker port |
@@ -463,6 +464,17 @@ meshcore/+/command/request_telemetry
| MQTT_PASSWORD | | MQTT password (optional) |
| MQTT_PREFIX | meshcore | Topic prefix |
### Data Directory Structure
The `DATA_HOME` environment variable controls where all service data is stored:
```
${DATA_HOME}/
├── collector/
│ ├── meshcore.db # SQLite database
│ └── tags.json # Node tags for import
└── web/
└── members.json # Network members list
```
### Interface
| Variable | Default | Description |
|----------|---------|-------------|
@@ -474,7 +486,8 @@ meshcore/+/command/request_telemetry
### Collector
| Variable | Default | Description |
|----------|---------|-------------|
| DATABASE_URL | sqlite:///./meshcore.db | SQLAlchemy URL |
| DATABASE_URL | sqlite:///{DATA_HOME}/collector/meshcore.db | SQLAlchemy URL |
| TAGS_FILE | {DATA_HOME}/collector/tags.json | Path to tags JSON file |
### API
| Variable | Default | Description |
@@ -483,7 +496,7 @@ meshcore/+/command/request_telemetry
| API_PORT | 8000 | API bind port |
| API_READ_KEY | | Read-only API key |
| API_ADMIN_KEY | | Admin API key |
| DATABASE_URL | sqlite:///./meshcore.db | SQLAlchemy URL |
| DATABASE_URL | sqlite:///{DATA_HOME}/collector/meshcore.db | SQLAlchemy URL |
### Web Dashboard
| Variable | Default | Description |
@@ -500,7 +513,7 @@ meshcore/+/command/request_telemetry
| NETWORK_RADIO_CONFIG | | Radio config details |
| NETWORK_CONTACT_EMAIL | | Contact email |
| NETWORK_CONTACT_DISCORD | | Discord link |
| MEMBERS_FILE | members.json | Path to members JSON |
| MEMBERS_FILE | {DATA_HOME}/web/members.json | Path to members JSON |
---
+13 -1
View File
@@ -21,10 +21,22 @@ target_metadata = Base.metadata
def get_database_url() -> str:
"""Get database URL from environment or config."""
# First try environment variable
from pathlib import Path
# First try explicit DATABASE_URL environment variable
url = os.environ.get("DATABASE_URL")
if url:
# Ensure directory exists for sqlite URLs
if url.startswith("sqlite:///"):
db_path = Path(url.replace("sqlite:///", ""))
db_path.parent.mkdir(parents=True, exist_ok=True)
return url
# Try DATA_HOME environment variable
data_home = os.environ.get("DATA_HOME")
if data_home:
db_path = Path(data_home) / "collector" / "meshcore.db"
db_path.parent.mkdir(parents=True, exist_ok=True)
return f"sqlite:///{db_path}"
# Fall back to alembic.ini
return config.get_main_option("sqlalchemy.url", "sqlite:///./meshcore.db")
+25 -20
View File
@@ -17,9 +17,14 @@
# - migrate: Run database migrations
# - import-tags: Import node tags from JSON file
#
# Data Directory Structure:
# ./data/
# Data Directory (DATA_HOME):
# The DATA_HOME environment variable controls where all service data is stored.
# Default: ./data (local) or /data (in containers)
#
# Structure:
# ${DATA_HOME}/
# ├── collector/
# │ ├── meshcore.db # SQLite database
# │ └── tags.json # Node tags for import
# └── web/
# └── members.json # Network members list
@@ -175,9 +180,8 @@ services:
mqtt:
condition: service_healthy
volumes:
- meshcore_data:/data
# Mount collector data directory for tags.json (optional)
- ${COLLECTOR_DATA_PATH:-./data/collector}:/app/collector-data:ro
# Mount data directory (contains collector/meshcore.db and collector/tags.json)
- ${DATA_HOME:-./data}:/data
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- MQTT_HOST=${MQTT_HOST:-mqtt}
@@ -185,7 +189,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- DATABASE_URL=sqlite:////data/meshcore.db
- DATA_HOME=/data
# Webhook configuration
- WEBHOOK_ADVERTISEMENT_URL=${WEBHOOK_ADVERTISEMENT_URL:-}
- WEBHOOK_ADVERTISEMENT_SECRET=${WEBHOOK_ADVERTISEMENT_SECRET:-}
@@ -227,7 +231,8 @@ services:
ports:
- "${API_PORT:-8000}:8000"
volumes:
- meshcore_data:/data
# Mount data directory (uses collector/meshcore.db)
- ${DATA_HOME:-./data}:/data
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- MQTT_HOST=${MQTT_HOST:-mqtt}
@@ -235,7 +240,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- DATABASE_URL=sqlite:////data/meshcore.db
- DATA_HOME=/data
- API_HOST=0.0.0.0
- API_PORT=8000
- API_READ_KEY=${API_READ_KEY:-}
@@ -266,12 +271,16 @@ services:
condition: service_healthy
ports:
- "${WEB_PORT:-8080}:8080"
volumes:
# Mount data directory (uses web/members.json)
- ${DATA_HOME:-./data}:/data
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- API_BASE_URL=http://api:8000
- API_KEY=${API_READ_KEY:-}
- WEB_HOST=0.0.0.0
- WEB_PORT=8080
- DATA_HOME=/data
- NETWORK_NAME=${NETWORK_NAME:-MeshCore Network}
- NETWORK_CITY=${NETWORK_CITY:-}
- NETWORK_COUNTRY=${NETWORK_COUNTRY:-}
@@ -279,10 +288,6 @@ services:
- NETWORK_RADIO_CONFIG=${NETWORK_RADIO_CONFIG:-}
- NETWORK_CONTACT_EMAIL=${NETWORK_CONTACT_EMAIL:-}
- NETWORK_CONTACT_DISCORD=${NETWORK_CONTACT_DISCORD:-}
- MEMBERS_FILE=${MEMBERS_FILE:-/app/web-data/members.json}
volumes:
# Mount web data directory for members.json (optional)
- ${WEB_DATA_PATH:-./data/web}:/app/web-data:ro
command: ["web"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
@@ -302,9 +307,10 @@ services:
profiles:
- migrate
volumes:
- meshcore_data:/data
# Mount data directory (uses collector/meshcore.db)
- ${DATA_HOME:-./data}:/data
environment:
- DATABASE_URL=sqlite:////data/meshcore.db
- DATA_HOME=/data
command: ["db", "upgrade"]
# ==========================================================================
@@ -318,12 +324,13 @@ services:
profiles:
- import-tags
volumes:
- meshcore_data:/data
- ${COLLECTOR_DATA_PATH:-./data/collector}:/app/collector-data:ro
# Mount data directory (uses collector/tags.json and collector/meshcore.db)
- ${DATA_HOME:-./data}:/data
environment:
- DATABASE_URL=sqlite:////data/meshcore.db
- DATA_HOME=/data
- LOG_LEVEL=${LOG_LEVEL:-INFO}
command: ["collector", "import-tags", "/app/collector-data/tags.json"]
# Uses default tags file: /data/collector/tags.json
command: ["collector", "import-tags"]
# ==========================================================================
# Volumes
@@ -333,5 +340,3 @@ volumes:
name: meshcore_mosquitto_data
mosquitto_log:
name: meshcore_mosquitto_log
meshcore_data:
name: meshcore_data
+26 -5
View File
@@ -18,12 +18,19 @@ import click
envvar="API_PORT",
help="API server port",
)
@click.option(
"--data-home",
type=str,
default=None,
envvar="DATA_HOME",
help="Base data directory (default: ./data)",
)
@click.option(
"--database-url",
type=str,
default="sqlite:///./meshcore.db",
default=None,
envvar="DATABASE_URL",
help="Database connection URL",
help="Database connection URL (default: sqlite:///{data_home}/collector/meshcore.db)",
)
@click.option(
"--read-key",
@@ -78,7 +85,8 @@ def api(
ctx: click.Context,
host: str,
port: int,
database_url: str,
data_home: str | None,
database_url: str | None,
read_key: str | None,
admin_key: str | None,
mqtt_host: str,
@@ -108,14 +116,27 @@ def api(
"""
import uvicorn
from meshcore_hub.common.config import get_api_settings
from meshcore_hub.api.app import create_app
# Get settings to compute effective values
settings = get_api_settings()
# Override data_home if provided
if data_home:
settings = settings.model_copy(update={"data_home": data_home})
# Use effective database URL if not explicitly provided
effective_db_url = database_url if database_url else settings.effective_database_url
effective_data_home = data_home or settings.data_home
click.echo("=" * 50)
click.echo("MeshCore Hub API Server")
click.echo("=" * 50)
click.echo(f"Host: {host}")
click.echo(f"Port: {port}")
click.echo(f"Database: {database_url}")
click.echo(f"Data home: {effective_data_home}")
click.echo(f"Database: {effective_db_url}")
click.echo(f"MQTT: {mqtt_host}:{mqtt_port} (prefix: {mqtt_prefix})")
click.echo(f"Read key configured: {read_key is not None}")
click.echo(f"Admin key configured: {admin_key is not None}")
@@ -144,7 +165,7 @@ def api(
else:
# For production, create app directly
app = create_app(
database_url=database_url,
database_url=effective_db_url,
read_key=read_key,
admin_key=admin_key,
mqtt_host=mqtt_host,
+56 -9
View File
@@ -42,12 +42,19 @@ from meshcore_hub.common.logging import configure_logging
envvar="MQTT_PREFIX",
help="MQTT topic prefix",
)
@click.option(
"--data-home",
type=str,
default=None,
envvar="DATA_HOME",
help="Base data directory (default: ./data)",
)
@click.option(
"--database-url",
type=str,
default="sqlite:///./meshcore.db",
default=None,
envvar="DATABASE_URL",
help="Database connection URL",
help="Database connection URL (default: sqlite:///{data_home}/collector/meshcore.db)",
)
@click.option(
"--log-level",
@@ -63,7 +70,8 @@ def collector(
mqtt_username: str | None,
mqtt_password: str | None,
prefix: str,
database_url: str,
data_home: str | None,
database_url: str | None,
log_level: str,
) -> None:
"""Collector component for storing MeshCore events.
@@ -80,14 +88,30 @@ def collector(
When invoked without a subcommand, runs the collector service.
"""
from meshcore_hub.common.config import get_collector_settings
# Get settings to compute effective values
settings = get_collector_settings()
# Override data_home if provided
if data_home:
settings = get_collector_settings()
# Re-create settings with data_home override
settings = settings.model_copy(update={"data_home": data_home})
# Use effective database URL if not explicitly provided
effective_db_url = database_url if database_url else settings.effective_database_url
ctx.ensure_object(dict)
ctx.obj["mqtt_host"] = mqtt_host
ctx.obj["mqtt_port"] = mqtt_port
ctx.obj["mqtt_username"] = mqtt_username
ctx.obj["mqtt_password"] = mqtt_password
ctx.obj["prefix"] = prefix
ctx.obj["database_url"] = database_url
ctx.obj["data_home"] = data_home or settings.data_home
ctx.obj["database_url"] = effective_db_url
ctx.obj["log_level"] = log_level
ctx.obj["settings"] = settings
# If no subcommand, run the collector service
if ctx.invoked_subcommand is None:
@@ -97,8 +121,9 @@ def collector(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
prefix=prefix,
database_url=database_url,
database_url=effective_db_url,
log_level=log_level,
data_home=data_home or settings.data_home,
)
@@ -110,6 +135,7 @@ def _run_collector_service(
prefix: str,
database_url: str,
log_level: str,
data_home: str,
) -> None:
"""Run the collector service.
@@ -119,9 +145,16 @@ def _run_collector_service(
- WEBHOOK_CHANNEL_MESSAGE_URL: Override for channel messages
- WEBHOOK_DIRECT_MESSAGE_URL: Override for direct messages
"""
from pathlib import Path
configure_logging(level=log_level)
# Ensure data directory exists
collector_data_dir = Path(data_home) / "collector"
collector_data_dir.mkdir(parents=True, exist_ok=True)
click.echo("Starting MeshCore Collector")
click.echo(f"Data home: {data_home}")
click.echo(f"MQTT: {mqtt_host}:{mqtt_port} (prefix: {prefix})")
click.echo(f"Database: {database_url}")
@@ -171,11 +204,12 @@ def run_cmd(ctx: click.Context) -> None:
prefix=ctx.obj["prefix"],
database_url=ctx.obj["database_url"],
log_level=ctx.obj["log_level"],
data_home=ctx.obj["data_home"],
)
@collector.command("import-tags")
@click.argument("file", type=click.Path(exists=True))
@click.argument("file", type=click.Path(exists=True), required=False, default=None)
@click.option(
"--no-create-nodes",
is_flag=True,
@@ -185,7 +219,7 @@ def run_cmd(ctx: click.Context) -> None:
@click.pass_context
def import_tags_cmd(
ctx: click.Context,
file: str,
file: str | None,
no_create_nodes: bool,
) -> None:
"""Import node tags from a JSON file.
@@ -194,6 +228,7 @@ def import_tags_cmd(
into the database. Existing tags are updated, new tags are created.
FILE is the path to the JSON file containing tags.
If not provided, defaults to {DATA_HOME}/collector/tags.json.
Expected JSON format:
\b
@@ -210,9 +245,21 @@ def import_tags_cmd(
Supported value_type: string, number, boolean, coordinate
"""
from pathlib import Path
configure_logging(level=ctx.obj["log_level"])
click.echo(f"Importing tags from: {file}")
# Use effective tags file if not provided
settings = ctx.obj["settings"]
tags_file = file if file else settings.effective_tags_file
# Check if file exists when using default
if not file and not Path(tags_file).exists():
click.echo(f"Tags file not found: {tags_file}")
click.echo("Specify a file path or create the default tags file.")
return
click.echo(f"Importing tags from: {tags_file}")
click.echo(f"Database: {ctx.obj['database_url']}")
from meshcore_hub.common.database import DatabaseManager
@@ -224,7 +271,7 @@ def import_tags_cmd(
# Import tags
stats = import_tags(
file_path=file,
file_path=tags_file,
db=db,
create_nodes=not no_create_nodes,
)
+80 -17
View File
@@ -33,6 +33,12 @@ class CommonSettings(BaseSettings):
extra="ignore",
)
# Data home directory (base for all service data directories)
data_home: str = Field(
default="./data",
description="Base directory for service data (e.g., ./data or /data)",
)
# Logging
log_level: LogLevel = Field(default=LogLevel.INFO, description="Logging level")
@@ -68,10 +74,16 @@ class InterfaceSettings(CommonSettings):
class CollectorSettings(CommonSettings):
"""Settings for the Collector component."""
# Database
database_url: str = Field(
default="sqlite:///./meshcore.db",
description="SQLAlchemy database URL",
# Database - default uses data_home/collector/meshcore.db
database_url: Optional[str] = Field(
default=None,
description="SQLAlchemy database URL (default: sqlite:///{data_home}/collector/meshcore.db)",
)
# Tags file for import - default uses data_home/collector/tags.json
tags_file: Optional[str] = Field(
default=None,
description="Path to tags JSON file (default: {data_home}/collector/tags.json)",
)
# Webhook URLs (empty = disabled)
@@ -109,12 +121,37 @@ class CollectorSettings(CommonSettings):
default=2.0, description="Retry backoff multiplier"
)
@property
def collector_data_dir(self) -> str:
"""Get the collector data directory path."""
from pathlib import Path
return str(Path(self.data_home) / "collector")
@property
def effective_database_url(self) -> str:
"""Get the effective database URL, using default if not set."""
if self.database_url:
return self.database_url
from pathlib import Path
db_path = Path(self.data_home) / "collector" / "meshcore.db"
return f"sqlite:///{db_path}"
@property
def effective_tags_file(self) -> str:
"""Get the effective tags file path, using default if not set."""
if self.tags_file:
return self.tags_file
from pathlib import Path
return str(Path(self.data_home) / "collector" / "tags.json")
@field_validator("database_url")
@classmethod
def validate_database_url(cls, v: str) -> str:
def validate_database_url(cls, v: Optional[str]) -> Optional[str]:
"""Validate database URL format."""
if not v:
raise ValueError("Database URL cannot be empty")
# None is allowed - will use default
return v
@@ -125,10 +162,10 @@ class APISettings(CommonSettings):
api_host: str = Field(default="0.0.0.0", description="API server host")
api_port: int = Field(default=8000, description="API server port")
# Database
database_url: str = Field(
default="sqlite:///./meshcore.db",
description="SQLAlchemy database URL",
# Database - default uses data_home/collector/meshcore.db (same as collector)
database_url: Optional[str] = Field(
default=None,
description="SQLAlchemy database URL (default: sqlite:///{data_home}/collector/meshcore.db)",
)
# Authentication
@@ -137,12 +174,21 @@ class APISettings(CommonSettings):
default=None, description="Admin API key (full access)"
)
@property
def effective_database_url(self) -> str:
"""Get the effective database URL, using default if not set."""
if self.database_url:
return self.database_url
from pathlib import Path
db_path = Path(self.data_home) / "collector" / "meshcore.db"
return f"sqlite:///{db_path}"
@field_validator("database_url")
@classmethod
def validate_database_url(cls, v: str) -> str:
def validate_database_url(cls, v: Optional[str]) -> Optional[str]:
"""Validate database URL format."""
if not v:
raise ValueError("Database URL cannot be empty")
# None is allowed - will use default
return v
@@ -186,11 +232,28 @@ class WebSettings(CommonSettings):
default=None, description="Discord server link"
)
# Members file
members_file: str = Field(
default="members.json", description="Path to members JSON file"
# Members file - default uses data_home/web/members.json
members_file: Optional[str] = Field(
default=None,
description="Path to members JSON file (default: {data_home}/web/members.json)",
)
@property
def web_data_dir(self) -> str:
"""Get the web data directory path."""
from pathlib import Path
return str(Path(self.data_home) / "web")
@property
def effective_members_file(self) -> str:
"""Get the effective members file path, using default if not set."""
if self.members_file:
return self.members_file
from pathlib import Path
return str(Path(self.data_home) / "web" / "members.json")
def get_common_settings() -> CommonSettings:
"""Get common settings instance."""
+31 -4
View File
@@ -32,6 +32,13 @@ import click
envvar="API_KEY",
help="API key for queries",
)
@click.option(
"--data-home",
type=str,
default=None,
envvar="DATA_HOME",
help="Base data directory (default: ./data)",
)
@click.option(
"--network-name",
type=str,
@@ -93,7 +100,7 @@ import click
type=str,
default=None,
envvar="MEMBERS_FILE",
help="Path to members JSON file",
help="Path to members JSON file (default: {data_home}/web/members.json)",
)
@click.option(
"--reload",
@@ -108,6 +115,7 @@ def web(
port: int,
api_url: str,
api_key: str | None,
data_home: str | None,
network_name: str,
network_city: str | None,
network_country: str | None,
@@ -142,14 +150,34 @@ def web(
meshcore-hub web --reload
"""
import uvicorn
from pathlib import Path
from meshcore_hub.common.config import get_web_settings
from meshcore_hub.web.app import create_app
# Get settings to compute effective values
settings = get_web_settings()
# Override data_home if provided
if data_home:
settings = settings.model_copy(update={"data_home": data_home})
# Use effective members file if not explicitly provided
effective_members_file = (
members_file if members_file else settings.effective_members_file
)
effective_data_home = data_home or settings.data_home
# Ensure web data directory exists
web_data_dir = Path(effective_data_home) / "web"
web_data_dir.mkdir(parents=True, exist_ok=True)
click.echo("=" * 50)
click.echo("MeshCore Hub Web Dashboard")
click.echo("=" * 50)
click.echo(f"Host: {host}")
click.echo(f"Port: {port}")
click.echo(f"Data home: {effective_data_home}")
click.echo(f"API URL: {api_url}")
click.echo(f"API key configured: {api_key is not None}")
click.echo(f"Network: {network_name}")
@@ -157,8 +185,7 @@ def web(
click.echo(f"Location: {network_city}, {network_country}")
if network_lat != 0.0 or network_lon != 0.0:
click.echo(f"Map center: {network_lat}, {network_lon}")
if members_file:
click.echo(f"Members file: {members_file}")
click.echo(f"Members file: {effective_members_file}")
click.echo(f"Reload mode: {reload}")
click.echo("=" * 50)
@@ -188,7 +215,7 @@ def web(
network_radio_config=network_radio_config,
network_contact_email=network_contact_email,
network_contact_discord=network_contact_discord,
members_file=members_file,
members_file=effective_members_file,
)
click.echo("\nStarting web dashboard...")
+73 -9
View File
@@ -1,7 +1,5 @@
"""Tests for configuration settings."""
import pytest
from meshcore_hub.common.config import (
CommonSettings,
InterfaceSettings,
@@ -20,6 +18,7 @@ class TestCommonSettings:
"""Test default setting values without .env file influence."""
settings = CommonSettings(_env_file=None)
assert settings.data_home == "./data"
assert settings.log_level == LogLevel.INFO
assert settings.mqtt_host == "localhost"
assert settings.mqtt_port == 1883
@@ -27,6 +26,12 @@ class TestCommonSettings:
assert settings.mqtt_password is None
assert settings.mqtt_prefix == "meshcore"
def test_custom_data_home(self) -> None:
"""Test custom DATA_HOME setting."""
settings = CommonSettings(_env_file=None, data_home="/custom/data")
assert settings.data_home == "/custom/data"
class TestInterfaceSettings:
"""Tests for InterfaceSettings."""
@@ -48,12 +53,34 @@ class TestCollectorSettings:
"""Test default setting values without .env file influence."""
settings = CollectorSettings(_env_file=None)
assert settings.database_url == "sqlite:///./meshcore.db"
# database_url is None by default, effective_database_url computes it
assert settings.database_url is None
# Path normalizes ./data to data
assert settings.effective_database_url == "sqlite:///data/collector/meshcore.db"
assert settings.data_home == "./data"
assert settings.collector_data_dir == "data/collector"
assert settings.tags_file is None
assert settings.effective_tags_file == "data/collector/tags.json"
def test_database_url_validation(self) -> None:
"""Test database URL validation."""
with pytest.raises(ValueError):
CollectorSettings(_env_file=None, database_url="")
def test_custom_data_home(self) -> None:
"""Test that custom data_home affects effective paths."""
settings = CollectorSettings(_env_file=None, data_home="/custom/data")
assert (
settings.effective_database_url
== "sqlite:////custom/data/collector/meshcore.db"
)
assert settings.collector_data_dir == "/custom/data/collector"
assert settings.effective_tags_file == "/custom/data/collector/tags.json"
def test_explicit_database_url_overrides(self) -> None:
"""Test that explicit database_url overrides the default."""
settings = CollectorSettings(
_env_file=None, database_url="postgresql://user@host/db"
)
assert settings.database_url == "postgresql://user@host/db"
assert settings.effective_database_url == "postgresql://user@host/db"
class TestAPISettings:
@@ -65,10 +92,29 @@ class TestAPISettings:
assert settings.api_host == "0.0.0.0"
assert settings.api_port == 8000
assert settings.database_url == "sqlite:///./meshcore.db"
# database_url is None by default, effective_database_url computes it
assert settings.database_url is None
# Path normalizes ./data to data
assert settings.effective_database_url == "sqlite:///data/collector/meshcore.db"
assert settings.api_read_key is None
assert settings.api_admin_key is None
def test_custom_data_home(self) -> None:
"""Test that custom data_home affects effective database path."""
settings = APISettings(_env_file=None, data_home="/custom/data")
assert (
settings.effective_database_url
== "sqlite:////custom/data/collector/meshcore.db"
)
def test_explicit_database_url_overrides(self) -> None:
"""Test that explicit database_url overrides the default."""
settings = APISettings(_env_file=None, database_url="postgresql://user@host/db")
assert settings.database_url == "postgresql://user@host/db"
assert settings.effective_database_url == "postgresql://user@host/db"
class TestWebSettings:
"""Tests for WebSettings."""
@@ -81,4 +127,22 @@ class TestWebSettings:
assert settings.web_port == 8080
assert settings.api_base_url == "http://localhost:8000"
assert settings.network_name == "MeshCore Network"
assert settings.members_file == "members.json"
# members_file is None by default, effective_members_file computes it
assert settings.members_file is None
# Path normalizes ./data to data
assert settings.effective_members_file == "data/web/members.json"
assert settings.web_data_dir == "data/web"
def test_custom_data_home(self) -> None:
"""Test that custom data_home affects effective paths."""
settings = WebSettings(_env_file=None, data_home="/custom/data")
assert settings.effective_members_file == "/custom/data/web/members.json"
assert settings.web_data_dir == "/custom/data/web"
def test_explicit_members_file_overrides(self) -> None:
"""Test that explicit members_file overrides the default."""
settings = WebSettings(_env_file=None, members_file="/path/to/members.json")
assert settings.members_file == "/path/to/members.json"
assert settings.effective_members_file == "/path/to/members.json"