Files
AR-ElecArrangement/backend/arelec/core/project.py
T

120 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Raíz del proyecto y serialización al formato ``.area``.
Un ``.area`` es un archivo ZIP que contiene:
::
project.json — metadata del proyecto y modelo serializado
assets/ — recursos (íconos custom, capturas, fotografías)
El formato JSON usa el modelo declarado por el resto de ``arelec.core`` y
está versionado por el campo ``schema_version``. Los .area producidos por
una versión del software pueden ser leídos por versiones posteriores
(migraciones aditivas), nunca por anteriores.
En Sprint 0 el modelo es mínimo (solo metadata) — los sub-modelos (ship,
decks, appliances, panels, cables…) se agregan en sprints siguientes.
"""
from __future__ import annotations
import json
import zipfile
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
SCHEMA_VERSION = 1
@dataclass
class ProjectMetadata:
"""Datos administrativos del proyecto."""
name: str = "Nuevo proyecto"
author: str = ""
company: str = ""
notes: str = ""
schema_version: int = SCHEMA_VERSION
created_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
modified_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
@dataclass
class Project:
"""
Raíz del modelo de datos. Sprint 0 contiene solo metadata; los sub-modelos
(ship, decks, appliances, panels, cable_runs, batteries, etc.) se acoplan
a esta clase en sprints siguientes.
La instancia se serializa íntegra como ``project.json`` dentro del ZIP
``.area``.
"""
metadata: ProjectMetadata = field(default_factory=ProjectMetadata)
# Sub-modelos futuros se irán agregando aquí:
# ship: Ship | None = None
# decks: list[Deck] = field(default_factory=list)
# appliances: list[Appliance] = field(default_factory=list)
# batteries: list[BatteryBank] = field(default_factory=list)
# panels: list[Panel] = field(default_factory=list)
# cable_runs: list[CableRun] = field(default_factory=list)
# ── Serialization ───────────────────────────────────────────────────────
def to_json(self) -> dict[str, Any]:
"""Convertir a dict serializable (JSON-safe)."""
return asdict(self)
@classmethod
def from_json(cls, data: dict[str, Any]) -> "Project":
"""Reconstruir desde dict cargado de ``project.json``."""
meta_dict = data.get("metadata", {})
return cls(metadata=ProjectMetadata(**meta_dict))
# ── .area I/O ───────────────────────────────────────────────────────────
def save(self, path: str | Path) -> None:
"""
Persistir el proyecto a ``path`` (extensión ``.area`` sugerida).
Toca ``metadata.modified_at`` con la hora UTC actual antes de
serializar.
"""
path = Path(path)
self.metadata.modified_at = datetime.now(timezone.utc).isoformat()
payload = json.dumps(self.to_json(), indent=2, ensure_ascii=False)
# ``ZIP_DEFLATED`` para que los .area no crezcan absurdo en proyectos
# grandes — el contenido es texto y comprime ~10×.
with zipfile.ZipFile(path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("project.json", payload)
@classmethod
def load(cls, path: str | Path) -> "Project":
"""
Cargar un proyecto desde ``path``. Lanza:
- ``FileNotFoundError`` si el archivo no existe
- ``zipfile.BadZipFile`` si el archivo no es ZIP válido
- ``ValueError`` si ``schema_version`` es mayor que ``SCHEMA_VERSION``
(el .area fue creado por una versión más nueva del software)
- ``KeyError`` si falta ``project.json`` en el ZIP
"""
path = Path(path)
with zipfile.ZipFile(path, mode="r") as zf:
with zf.open("project.json") as f:
data = json.loads(f.read().decode("utf-8"))
version = data.get("metadata", {}).get("schema_version", 1)
if version > SCHEMA_VERSION:
raise ValueError(
f"El proyecto requiere schema_version={version}, "
f"este software soporta hasta {SCHEMA_VERSION}. "
f"Actualiza AR-ElecArrangement."
)
return cls.from_json(data)