Implement storage layer for MVP-1 Personal Tracker CLI
Add storage layer with FileStorage, MarkdownReader, and MarkdownWriter classes. Add data models (Project, Session, Note, Change).
This commit is contained in:
241
tracker/storage/markdown_writer.py
Normal file
241
tracker/storage/markdown_writer.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""Markdown writer utility."""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from tracker.models.session import Session
|
||||
|
||||
|
||||
class MarkdownWriter:
|
||||
"""Escritura de archivos Markdown del proyecto."""
|
||||
|
||||
def format_log_entry(self, session: Session, summary: str) -> str:
|
||||
"""Formatea una entrada para LOG.md.
|
||||
|
||||
Formato:
|
||||
## 2026-03-23 10:00–11:20
|
||||
**Objetivo**
|
||||
...
|
||||
|
||||
**Trabajo realizado**
|
||||
- ...
|
||||
|
||||
**Cambios relevantes**
|
||||
- ...
|
||||
|
||||
**Bloqueos**
|
||||
- ...
|
||||
|
||||
**Decisiones**
|
||||
- ...
|
||||
|
||||
**Próximos pasos**
|
||||
- ...
|
||||
|
||||
**Resumen**
|
||||
...
|
||||
|
||||
Returns string formateado.
|
||||
"""
|
||||
started = session.started_at.strftime("%Y-%m-%d %H:%M")
|
||||
ended = session.ended_at.strftime("%H:%M") if session.ended_at else "En Curso"
|
||||
date_range = f"{started}–{ended}"
|
||||
|
||||
lines = [
|
||||
f"## {date_range}",
|
||||
"",
|
||||
"**Objetivo**",
|
||||
f"{session.objective or 'No especificado'}",
|
||||
"",
|
||||
"**Trabajo realizado**",
|
||||
]
|
||||
|
||||
if session.work_done:
|
||||
for item in session.work_done:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin trabajo registrado")
|
||||
|
||||
lines.extend(["", "**Cambios relevantes**"])
|
||||
if session.changes:
|
||||
for item in session.changes:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin cambios")
|
||||
|
||||
lines.extend(["", "**Bloqueos**"])
|
||||
if session.blockers:
|
||||
for item in session.blockers:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin bloqueos")
|
||||
|
||||
lines.extend(["", "**Decisiones**"])
|
||||
if session.decisions:
|
||||
for item in session.decisions:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin decisiones")
|
||||
|
||||
lines.extend(["", "**Próximos pasos**"])
|
||||
if session.next_steps:
|
||||
for item in session.next_steps:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin pasos definidos")
|
||||
|
||||
lines.extend(["", "**Resumen**", summary])
|
||||
|
||||
return "\n".join(lines) + "\n\n"
|
||||
|
||||
def format_session_file(self, session: Session) -> str:
|
||||
"""Formatea archivo de sesion detalle en sessions/YYYY-MM-DD_HHMM.md.
|
||||
|
||||
Formato:
|
||||
# Sesion: 2026-03-23 10:00–11:20
|
||||
|
||||
## Objetivo
|
||||
...
|
||||
|
||||
## Notas
|
||||
...
|
||||
|
||||
## Trabajo realizado
|
||||
...
|
||||
|
||||
## Cambios
|
||||
...
|
||||
|
||||
## Decisiones
|
||||
...
|
||||
|
||||
## Bloqueos
|
||||
...
|
||||
|
||||
## Proximos pasos
|
||||
...
|
||||
|
||||
## Referencias
|
||||
...
|
||||
|
||||
## Duracion
|
||||
X minutos
|
||||
"""
|
||||
started = session.started_at.strftime("%Y-%m-%d %H:%M")
|
||||
ended = session.ended_at.strftime("%H:%M") if session.ended_at else "En Curso"
|
||||
|
||||
lines = [
|
||||
f"# Sesion: {started}–{ended}",
|
||||
"",
|
||||
"## Objetivo",
|
||||
f"{session.objective or 'No especificado'}",
|
||||
"",
|
||||
"## Notas",
|
||||
]
|
||||
|
||||
if session.raw_notes:
|
||||
for note in session.raw_notes:
|
||||
note_type = note.get("type", "work")
|
||||
note_text = note.get("text", "")
|
||||
lines.append(f"- [{note_type}] {note_text}")
|
||||
else:
|
||||
lines.append("- Sin notas")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Trabajo realizado",
|
||||
])
|
||||
if session.work_done:
|
||||
for item in session.work_done:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin trabajo realizado")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Cambios",
|
||||
])
|
||||
if session.changes:
|
||||
for item in session.changes:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin cambios")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Decisiones",
|
||||
])
|
||||
if session.decisions:
|
||||
for item in session.decisions:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin decisiones")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Bloqueos",
|
||||
])
|
||||
if session.blockers:
|
||||
for item in session.blockers:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin bloqueos")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Proximos pasos",
|
||||
])
|
||||
if session.next_steps:
|
||||
for item in session.next_steps:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin pasos definidos")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Referencias",
|
||||
])
|
||||
if session.references:
|
||||
for item in session.references:
|
||||
lines.append(f"- {item}")
|
||||
else:
|
||||
lines.append("- Sin referencias")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Duracion",
|
||||
f"{session.duration_minutes} minutos",
|
||||
])
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
def format_autogen_section(self, content: str, section: str, new_content: str) -> str:
|
||||
"""Reemplaza o inserta una seccion AUTOGEN en contenido Markdown.
|
||||
|
||||
Busca <!-- AUTOGEN:{section}_START --> ... <!-- AUTOGEN:{section}_END -->
|
||||
Si existe, reemplaza el contenido entre los marcadores.
|
||||
Si no existe, inserta la seccion al final.
|
||||
|
||||
Returns el contenido modificado.
|
||||
"""
|
||||
start_marker = f"<!-- AUTOGEN:{section}_START -->"
|
||||
end_marker = f"<!-- AUTOGEN:{section}_END -->"
|
||||
|
||||
full_marker = f"{start_marker}\n{new_content}\n{end_marker}"
|
||||
|
||||
# Buscar si existe la seccion
|
||||
pattern = rf"{re.escape(start_marker)}.*?{re.escape(end_marker)}"
|
||||
if re.search(pattern, content, re.DOTALL):
|
||||
# Reemplazar seccion existente
|
||||
return re.sub(pattern, full_marker, content, flags=re.DOTALL)
|
||||
else:
|
||||
# Insertar al final
|
||||
return content + "\n" + full_marker + "\n"
|
||||
|
||||
def format_readme_section(self, section: str, content: str) -> str:
|
||||
"""Formatea una seccion de README.md.
|
||||
|
||||
Para usar con format_autogen_section.
|
||||
"""
|
||||
return content
|
||||
Reference in New Issue
Block a user