120 lines
4.5 KiB
Python
120 lines
4.5 KiB
Python
"""
|
||
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)
|