feat: añadido primer intento de app hecha con fastapi
Build Docker Image / docker (push) Failing after 9s
Build Docker Image / docker (push) Failing after 9s
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout código
|
||||
run: |
|
||||
git clone https://fedesrv.ddns.net/git/${{ github.repository }}.git .
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login fedesrv.ddns.net -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||
|
||||
- name: Pull base image
|
||||
run: docker pull fedesrv.ddns.net/fede/void-musl-busybox:latest
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
docker build \
|
||||
-t fedesrv.ddns.net/fede/BMC-Renderer:latest \
|
||||
.
|
||||
|
||||
- name: Push image
|
||||
run: docker push fedesrv.ddns.net/fede/BMC-Renderer:latest
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
FROM fedesrv.ddns.net/fede/void-musl-busybox:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
# instalar python (si la imagen lo permite)
|
||||
RUN xbps-install -Sy python3 python3-pip || true
|
||||
|
||||
# instalar dependencias
|
||||
RUN pip3 install fastapi uvicorn || true
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8000"]
|
||||
@@ -0,0 +1,88 @@
|
||||
from fastapi import FastAPI, Request, Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import re
|
||||
from typing import Dict, Optional
|
||||
|
||||
app = FastAPI(title="Business Model Canvas Parser")
|
||||
|
||||
# Montar static files y templates
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# Mapeo de secciones del BMC
|
||||
BMC_SECTIONS = {
|
||||
"socios_clave": "Socios Clave",
|
||||
"actividades_clave": "Actividades Clave",
|
||||
"recursos_clave": "Recursos Clave",
|
||||
"propuesta_de_valor": "Propuesta de valor",
|
||||
"relacion_con_clientes": "Relación Con Clientes",
|
||||
"canales": "Canales",
|
||||
"segmentos": "Segmentos",
|
||||
"estructura_de_costos": "Estructura de Costos",
|
||||
"fuentes_de_ingresos": "Fuentes de Ingresos",
|
||||
}
|
||||
|
||||
|
||||
def parse_markdown_to_bmc(markdown_text: str) -> Dict[str, str]:
|
||||
"""
|
||||
Parsea el markdown y extrae el contenido de cada sección del BMC
|
||||
"""
|
||||
bmc_data = {key: "" for key in BMC_SECTIONS.keys()}
|
||||
|
||||
# Patrones para buscar cada sección (case insensitive)
|
||||
patterns = {
|
||||
"socios_clave": r"##\s*Socios\s*Clave.*?(?=##|$)",
|
||||
"actividades_clave": r"##\s*Actividades\s*Clave.*?(?=##|$)",
|
||||
"recursos_clave": r"##\s*Recursos\s*Clave.*?(?=##|$)",
|
||||
"propuesta_de_valor": r"##\s*Propuesta\s*de\s*valor.*?(?=##|$)",
|
||||
"relacion_con_clientes": r"##\s*Relacion\s*Con\s*Clientes.*?(?=##|$)",
|
||||
"canales": r"##\s*Canales.*?(?=##|$)",
|
||||
"segmentos": r"##\s*Segmentos.*?(?=##|$)",
|
||||
"estructura_de_costos": r"##\s*Estructura\s*de\s*Costos.*?(?=##|$)",
|
||||
"fuentes_de_ingresos": r"##\s*Fuentes\s*de\s*Ingresos.*?(?=##|$)",
|
||||
}
|
||||
|
||||
for key, pattern in patterns.items():
|
||||
match = re.search(pattern, markdown_text, re.IGNORECASE | re.DOTALL)
|
||||
if match:
|
||||
content = match.group(0)
|
||||
# Remover el header y limpiar
|
||||
content = re.sub(r"^##.*\n", "", content, flags=re.IGNORECASE)
|
||||
# Remover numeración inicial si existe
|
||||
content = re.sub(r"^\d+\s*", "", content.strip())
|
||||
key2 = key
|
||||
bmc_data[key2] = content.strip()
|
||||
|
||||
return bmc_data
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def home(request: Request):
|
||||
return templates.TemplateResponse(request=request, name="bmc.html")
|
||||
|
||||
|
||||
@app.post("/render", response_class=HTMLResponse)
|
||||
async def render_bmc(request: Request, markdown_text: str = Form()):
|
||||
"""Procesa el markdown y renderiza el BMC"""
|
||||
bmc_data = parse_markdown_to_bmc(markdown_text)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="bmc.html",
|
||||
context={"markdown_text": markdown_text, "bmc_data": bmc_data},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/parse")
|
||||
async def api_parse_bmc(markdown_text: str = Form(...)):
|
||||
"""API endpoint que retorna JSON con los datos parseados"""
|
||||
bmc_data = parse_markdown_to_bmc(markdown_text)
|
||||
return {"bmc": bmc_data}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
@@ -0,0 +1,241 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: black ;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.input-section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.input-section label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.input-section textarea {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-section textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.input-section button {
|
||||
margin-top: 15px;
|
||||
padding: 12px 30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input-section button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* BMC Grid Layout */
|
||||
.bmc-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.bmc-block {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.bmc-block:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.bmc-block h3 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.bmc-block .content {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #444;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Colores específicos para cada bloque */
|
||||
.socios {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
.socios h3 {
|
||||
color: #e74c3c;
|
||||
border-color: #e74c3c;
|
||||
}
|
||||
|
||||
.actividades {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.actividades h3 {
|
||||
color: #3498db;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.propuesta {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / span 2;
|
||||
background-color: #c3cfe2);
|
||||
}
|
||||
.propuesta h3 {
|
||||
color: #2c3e50;
|
||||
border-color: #2c3e50;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.relaciones {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
}
|
||||
.relaciones h3 {
|
||||
color: #9b59b6;
|
||||
border-color: #9b59b6;
|
||||
}
|
||||
|
||||
.segmentos {
|
||||
grid-column: 5;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
.segmentos h3 {
|
||||
color: #1abc9c;
|
||||
border-color: #1abc9c;
|
||||
}
|
||||
|
||||
.recursos {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
.recursos h3 {
|
||||
color: #e67e22;
|
||||
border-color: #e67e22;
|
||||
}
|
||||
|
||||
.canales {
|
||||
grid-column: 4;
|
||||
grid-row: 2;
|
||||
}
|
||||
.canales h3 {
|
||||
color: #f39c12;
|
||||
border-color: #f39c12;
|
||||
}
|
||||
|
||||
.costos h3 {
|
||||
color: #c0392b;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
.last {
|
||||
grid-row: 3;
|
||||
grid-column: 1 / -1; /* ocupa TODAS las columnas */
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.costos{
|
||||
width: 50%;
|
||||
}
|
||||
.ingresos{
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.ingresos h3 {
|
||||
color: #27ae60;
|
||||
border-color: #27ae60;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.bmc-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
.propuesta {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.socios { grid-column: 1; grid-row: 1; }
|
||||
.actividades { grid-column: 3; grid-row: 1; }
|
||||
.relaciones { grid-column: 1; grid-row: 3; }
|
||||
.segmentos { grid-column: 3; grid-row: 3; }
|
||||
.recursos { grid-column: 2; grid-row: 1; }
|
||||
.canales { grid-column: 2; grid-row: 3; }
|
||||
.costos { grid-column: 1 / span 3; grid-row: 4; }
|
||||
.ingresos { grid-column: 1 / span 3; grid-row: 5; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bmc-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bmc-block {
|
||||
grid-column: 1 !important;
|
||||
grid-row: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Business Model Canvas</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Business Model Canvas Parser</h1>
|
||||
|
||||
<!-- Formulario de entrada -->
|
||||
<form method="post" action="/render" class="input-section">
|
||||
<label for="markdown">Pega tu markdown aquí:</label>
|
||||
<textarea name="markdown_text" rows="15" id="markdown_text" placeholder="# Business Model Canvas...
|
||||
|
||||
## Socios Clave...
|
||||
## Actividades Clave...
|
||||
## Recursos Clave...
|
||||
## Propuesta de valor...
|
||||
## Relacion Con Clientes...
|
||||
## Canales...
|
||||
## Segmentos...
|
||||
## Estructura de Costos...
|
||||
## Fuentes de Ingresos...">{{ markdown_text }}</textarea>
|
||||
<button type="submit">Generar BMC</button>
|
||||
</form>
|
||||
|
||||
{% if bmc_data %}
|
||||
<!-- Business Model Canvas Grid -->
|
||||
<div class="bmc-container">
|
||||
<!-- Fila 1: Socios, Actividades, Propuesta, Relaciones, Segmentos -->
|
||||
<div class="bmc-block socios">
|
||||
<h3>Socios Clave</h3>
|
||||
<div class="content">{{ bmc_data.socios_clave | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bmc-block actividades">
|
||||
<h3>Actividades Clave</h3>
|
||||
<div class="content">{{ bmc_data.actividades_clave | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bmc-block propuesta">
|
||||
<h3>Propuesta de Valor</h3>
|
||||
<div class="content">{{ bmc_data.propuesta_de_valor | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bmc-block relaciones">
|
||||
<h3>Relación con Clientes</h3>
|
||||
<div class="content">{{ bmc_data.relacion_con_clientes | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bmc-block segmentos">
|
||||
<h3>Segmentos de Clientes</h3>
|
||||
<div class="content">{{ bmc_data.segmentos | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 2: Recursos, Canales -->
|
||||
<div class="bmc-block recursos">
|
||||
<h3>Recursos Clave</h3>
|
||||
<div class="content">{{ bmc_data.recursos_clave | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bmc-block canales">
|
||||
<h3>Canales</h3>
|
||||
<div class="content">{{ bmc_data.canales | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila 3: Costos e Ingresos -->
|
||||
<div class="last">
|
||||
<div class="bmc-block costos">
|
||||
<h3>Estructura de Costos</h3>
|
||||
<div class="content">{{ bmc_data.estructura_de_costos | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bmc-block ingresos">
|
||||
<h3>Fuentes de Ingresos</h3>
|
||||
<div class="content">{{ bmc_data.fuentes_de_ingresos | replace('\n', '<br>') | safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user