Files
AR-Autopilot/arautopilot/core/project_config.py
T
alro65 8d4a698144 polish(sprint-0): clean code per ruff + mypy strict
Run the dev linters over Sprint 0's core/library/shared modules and
address every finding. Behaviour unchanged; tests still 80/80 green.

Changes:

- Replace `class Foo(str, Enum)` with `class Foo(StrEnum)` (PEP 663
  / Python 3.11+) in 7 enum classes: ActuatorType, AlarmSeverity,
  AlarmType, KnobMode, KnobFunction, AutopilotMode, AccessLevel,
  VesselType. Pydantic v2 serialises StrEnum the same way, so YAML/JSON
  round-trips are byte-identical.
- Use `datetime.UTC` alias in place of `datetime.timezone.utc`
  (UP017) across alarms.py, knob_state.py, project_config.py, and
  test_knob_state.py.
- Remove now-unnecessary forward-reference quotes from method return
  type annotations (UP037) — `from __future__ import annotations` is
  already in scope everywhere.
- Tighten `_read_json_resource` / `_read_yaml_resource` in the library
  loader: validate that the deserialised payload is actually a dict
  before returning, instead of leaking `Any` from json.loads /
  yaml.safe_load. Fixes the only two `mypy --strict` findings.
- Add `.claude/settings.local.json` to .gitignore (personal
  Claude Code overrides are not committed).

Verification:
  ruff check arautopilot/                 -> All checks passed
  mypy arautopilot/core library shared    -> Success, 0 issues, 12 files
  pytest                                  -> 80 passed in 0.25s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:26:37 -04:00

104 lines
3.8 KiB
Python

"""Root project entity — what a Studio ``.appack`` package contains.
A ``ProjectConfig`` is the unit of work in the Studio: one customer
project = one configured vessel. It serialises to YAML or JSON for
transport into the firmware build pipeline.
"""
from __future__ import annotations
import json
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
from pydantic import BaseModel, ConfigDict, Field
from arautopilot.core.ids import ProjectId, new_project_id
from arautopilot.core.vessel_config import VesselConfig
from arautopilot.version import __version__
class ProjectConfig(BaseModel):
"""Root configuration object persisted to disk and shipped as ``.appack``."""
model_config = ConfigDict(extra="forbid", validate_assignment=True)
schema_version: str = Field(default="0.1.0", pattern=r"^\d+\.\d+\.\d+$")
"""Project file schema version (independent of package ``__version__``)."""
package_version: str = Field(default=__version__)
"""Version of ``arautopilot`` that produced this file (informational)."""
project_id: ProjectId = Field(default_factory=new_project_id)
client_name: str = Field(min_length=1, max_length=120)
project_name: str = Field(min_length=1, max_length=120)
notes: str = Field(default="", max_length=2000)
vessel: VesselConfig
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
modified_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
# --- Serialisation -------------------------------------------------------
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-ready dict (datetimes as ISO 8601 strings)."""
return self.model_dump(mode="json")
def to_json(self, *, indent: int | None = 2) -> str:
"""Serialise to JSON text."""
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
def to_yaml(self) -> str:
"""Serialise to YAML text (block style, sorted keys disabled to preserve schema order)."""
return yaml.safe_dump(
self.to_dict(),
sort_keys=False,
default_flow_style=False,
allow_unicode=True,
)
def save_yaml(self, path: Path | str) -> Path:
"""Write the project to ``path`` as YAML and return the resolved path."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(self.to_yaml(), encoding="utf-8")
return p
def save_json(self, path: Path | str) -> Path:
"""Write the project to ``path`` as JSON and return the resolved path."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(self.to_json(), encoding="utf-8")
return p
# --- Deserialisation -----------------------------------------------------
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
return cls.model_validate(data)
@classmethod
def from_json(cls, text: str) -> ProjectConfig:
return cls.model_validate(json.loads(text))
@classmethod
def from_yaml(cls, text: str) -> ProjectConfig:
return cls.model_validate(yaml.safe_load(text))
@classmethod
def load(cls, path: Path | str) -> ProjectConfig:
"""Load from disk; format inferred from the file extension."""
p = Path(path)
text = p.read_text(encoding="utf-8")
suffix = p.suffix.lower()
if suffix in (".yaml", ".yml"):
return cls.from_yaml(text)
if suffix == ".json":
return cls.from_json(text)
raise ValueError(f"Unsupported project file extension: {suffix!r}")
def touch(self) -> ProjectConfig:
"""Return a copy with ``modified_at`` refreshed to now (UTC)."""
return self.model_copy(update={"modified_at": datetime.now(UTC)})