Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
lint:
name: Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install ruff
run: pip install ruff==0.15.17
- name: Lint
run: ruff check .

build:
name: Build & import (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install core and pure-Python plugins
run: |
pip install ./core
pip install \
./plugins/message_sources/mock_message_source \
./plugins/message_processors/ais_decoder_processor \
./plugins/renderers/image_renderer
- name: Import smoke test
run: python -c "import vf_core.main, mock_message_source, ais_decoder_processor, image_renderer"
5 changes: 5 additions & 0 deletions core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ dependencies = [
"pillow>=12.0.0"
]

[project.optional-dependencies]
dev = [
"ruff==0.15.17",
]

[project.scripts]
vf = "vf_core.main:main"

Expand Down
3 changes: 2 additions & 1 deletion core/src/vf_core/asset_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from pathlib import Path

from PIL import ImageFont
from dataclasses import dataclass


@dataclass
Expand Down
7 changes: 4 additions & 3 deletions core/src/vf_core/config_manager.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import copy
import tomllib
import tomli_w
from pathlib import Path
from typing import Any
import copy

import tomli_w


class ConfigManager:
Expand Down Expand Up @@ -67,7 +68,7 @@ def get(self, key: str, default: Any = None) -> Any:
Returns:
Any: A deep copy of the config value, or the default if not found.
"""

keys = key.split(".")
value = self._cfg

Expand Down
24 changes: 12 additions & 12 deletions core/src/vf_core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,33 @@

import argparse
import asyncio
from contextlib import suppress
from functools import partial
import logging
import os
import signal
import sys
from contextlib import suppress
from functools import partial
from logging.handlers import RotatingFileHandler
from pathlib import Path

from logging.handlers import RotatingFileHandler
from .asset_manager import AssetManager
from .config_manager import ConfigManager
from .message_bus import MessageBus
from .network_manager import NetworkManager
from .plugin_manager import PluginManager
from .plugin_types import (
GROUP_CONTROLLERS,
GROUP_PROCESSORS,
GROUP_RENDERER,
GROUP_SOURCES,
GROUP_CONTROLLERS,
Plugin,
RendererPlugin,
)
from .message_bus import MessageBus
from .plugin_manager import PluginManager
from .config_manager import ConfigManager
from .screen_manager import ScreenManager
from .vessel_manager import VesselManager
from .vessel_repository import VesselRepository
from .screen_manager import ScreenManager
from .network_manager import NetworkManager
from .asset_manager import AssetManager
from .web_admin.main import start_admin_server
from .web_admin import auth
from .web_admin.main import start_admin_server


def _default_data_dir() -> Path:
Expand Down Expand Up @@ -78,7 +78,7 @@ def _log_admin_status(
if task.cancelled():
# Manual shutdown - nothing to report
return

try:
task.result()
logger.warning("Admin server stopped unexpectedly but cleanly")
Expand Down
1 change: 1 addition & 0 deletions core/src/vf_core/marine_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""AIS data and formatting helpers."""
from __future__ import annotations

import math
from typing import NamedTuple

Expand Down
5 changes: 3 additions & 2 deletions core/src/vf_core/message_bus.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
from collections import defaultdict
from typing import Any, AsyncIterator
from collections.abc import AsyncIterator
from typing import Any

_SHUTDOWN = object()

Expand Down Expand Up @@ -61,7 +62,7 @@ async def subscribe(self, topic: str) -> AsyncIterator[Any]:
Yields:
Any: Messages published to the specified topic in the order received.
"""

q: asyncio.Queue[Any] = asyncio.Queue(maxsize=1000)
async with self._lock:
self._subs[topic].append(q)
Expand Down
12 changes: 6 additions & 6 deletions core/src/vf_core/network_manager.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import subprocess
import asyncio
import json
import time
import logging
import subprocess
import time
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from dataclasses import dataclass, asdict
from datetime import datetime
import asyncio


@dataclass
Expand Down Expand Up @@ -59,7 +59,7 @@ def _load_config(self) -> NetworkConfig:

try:
if self.CONFIG_FILE.exists():
with open(self.CONFIG_FILE, "r") as f:
with open(self.CONFIG_FILE) as f:
data = json.load(f)
return NetworkConfig.from_dict(data)
except Exception:
Expand Down
6 changes: 3 additions & 3 deletions core/src/vf_core/plugin_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from importlib.metadata import entry_points, EntryPoint
from collections.abc import Iterable, Callable
from typing import Any
import logging
from collections.abc import Callable, Iterable
from importlib.metadata import EntryPoint, entry_points
from typing import Any

from .plugin_types import GROUP_SCHEMAS, ConfigFieldType

Expand Down
4 changes: 2 additions & 2 deletions core/src/vf_core/plugin_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Protocol, runtime_checkable, Any, TYPE_CHECKING
from enum import Enum
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable

if TYPE_CHECKING:
from PIL import Image
Expand Down
5 changes: 3 additions & 2 deletions core/src/vf_core/render_strategies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import asyncio
import logging
import time
from typing import Callable, Awaitable, Any
from collections.abc import Awaitable, Callable
from contextlib import suppress
import logging
from typing import Any


class PeriodicRenderStrategy:
Expand Down
4 changes: 2 additions & 2 deletions core/src/vf_core/screen_manager.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import asyncio
import logging
from contextlib import suppress

from pathlib import Path

from vf_core.config_manager import ConfigManager

from .asset_manager import AssetManager
from .error_screen import ErrorScreen
from .message_bus import MessageBus
from .plugin_manager import PluginManager
from .plugin_types import GROUP_SCREENS, RendererPlugin, ScreenPlugin
from .vessel_manager import VesselManager
from .asset_manager import AssetManager


class ScreenManager:
Expand Down
1 change: 1 addition & 0 deletions core/src/vf_core/text_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
anchored text drawing and font metric helpers.
"""
from __future__ import annotations

from PIL import ImageDraw, ImageFont

__all__ = ["FONT_FLOOR", "split_two", "TextRenderingMixin"]
Expand Down
5 changes: 3 additions & 2 deletions core/src/vf_core/vessel_manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import asyncio
import time
import logging
import math
import time
from contextlib import suppress
from .message_bus import MessageBus
from typing import Any

from .message_bus import MessageBus
from .vessel_repository import VesselRepository


Expand Down
3 changes: 2 additions & 1 deletion core/src/vf_core/vessel_repository.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import json
import aiosqlite
import logging
from pathlib import Path
from typing import Any

import aiosqlite


class VesselRepository:
def __init__(self, db_path: Path | str) -> None:
Expand Down
15 changes: 8 additions & 7 deletions core/src/vf_core/web_admin/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta

import jwt
from argon2 import PasswordHasher, Type
from argon2.exceptions import VerifyMismatchError
from fastapi import APIRouter, HTTPException, Depends, status
import jwt
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel

from ..auth import (
get_or_create_secret_key,
get_admin_credentials,
set_admin_credentials,
get_or_create_secret_key,
is_admin_configured,
set_admin_credentials,
)

ph = PasswordHasher(type=Type.ID)
Expand Down Expand Up @@ -57,13 +58,13 @@ async def login(
try:
ph.verify(credentials["password_hash"], request.password)
except VerifyMismatchError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") from None

# Generate JWT
token = jwt.encode(
{
"username": request.username,
"exp": datetime.now(timezone.utc) + timedelta(days=7),
"exp": datetime.now(UTC) + timedelta(days=7),
},
secret_key,
algorithm="HS256",
Expand Down
8 changes: 4 additions & 4 deletions core/src/vf_core/web_admin/api/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from fastapi import APIRouter, HTTPException, Depends, status
from pydantic import BaseModel
from typing import Any
import logging
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from vf_core.config_manager import ConfigManager
from vf_core.plugin_manager import PluginManager
from vf_core.plugin_types import GROUP_SCHEMAS, ConfigField, ConfigFieldType
Expand Down Expand Up @@ -107,7 +107,7 @@ async def update_config(
cm.save()
return {"success": True, "path": update.path, "value": value}
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e


def _require_dict_keys(value: Any, keys: tuple[str, ...], label: str) -> None:
Expand Down
Loading
Loading