feat: AR-ElecArrangement initial commit — Python FastAPI + uvicorn (LAN desktop app, packaged as .exe via PyInstaller)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user