Instalación

Comandos de Instalación
$ python -m venv venv # O python3 -m venv venv
$ source venv/bin/activate # En Windows usa `venv\Scripts\activate`
$ pip install lila-framework
$ lila-init
$ python main.py # O python3 main.py

Configuración de la Aplicación

Propósito: Configurar e iniciar la aplicación. main.py es el punto de entrada principal donde se configura e inicializa la aplicación.

Explicación de main.py

from lila.core.app import App
from app.routes.routes import routes
from app.routes.api import routes as api_routes
from lila.core.middleware import Middleware
from app.config import PORT, HOST, DEBUG
from app.middlewares.defaults import (
    LoggingMiddleware,
    IPBlockingMiddleware,
    URLBlockingMiddleware,
    MaliciousExtensionMiddleware,
    SensitivePathMiddleware,
    ErrorHandlerMiddleware,
)
import itertools
import uvicorn
import asyncio

# English: Combining application and API routes into a single list.
# Español: Combinando las rutas de la aplicación y la API en una única lista.
all_routes = list(itertools.chain(routes, api_routes))

# English: Here we activate the admin panel with default settings.
# Español: Aquí activamos el panel de administrador con configuraciones predeterminadas.
# from app.routes.admin import Admin
# from app.models.user import User
# admin_routes=Admin(models=[User])
# all_routes = list(itertools.chain(routes, api_routes,admin_routes))
# English: Marker for the admin routes in main.py.
# Español: Marcador para las rutas de administrador en main.py.
# admin_marker

cors = None

# English: CORS usage example
# Español : Ejemplo de utilización de CORS
# cors={
#     "origin": ["*"],
#     "allow_credentials" : True,
#     "allow_methods":["*"],
#     "allow_headers": ["*"]
# }
# app = App(debug=True, routes=all_routes,cors=cors)

# English:necessary for cli command modify react cors for development
# Español:necesario para el comando cli modificar cors de react para desarrollo
# react_marker

middlewares = [
    Middleware(LoggingMiddleware),
    Middleware(IPBlockingMiddleware),
    Middleware(URLBlockingMiddleware),
    Middleware(MaliciousExtensionMiddleware),
    Middleware(SensitivePathMiddleware),
    Middleware(ErrorHandlerMiddleware),
]

# English: Initializing the application with debugging enabled and the combined routes.
# Español: Inicializando la aplicación con la depuración activada y las rutas combinadas.
app = App(debug=DEBUG, routes=all_routes, cors=cors, middleware=middlewares)

# English: To ensure SEO (bots, AI), caching, and HTML hydration, uncomment these lines.
# Adding {% include "react/cache/index.html" %} to 
# Español :Para tener seo (bots,ia) ,cache y que react hidrate el html ,descomenta estas lineas. # Agregadando en
, {% include "react/cache/index.html" %} # import subprocess # import sys # @app.on_event("startup") # async def startup_event(): # print("♻️ Prerender for react...") # url_with_port =f" http://{HOST}:{PORT}" # subprocess.Popen([ # sys.executable, # "-m", # "cli.prerender", # "--url", # url_with_port.strip() # ]) # English: Asynchronous main function to run the application server. # Español: Función principal asíncrona para ejecutar el servidor de la aplicación. async def main(): # English: Starting the Uvicorn server with the application instance. # Español: Iniciando el servidor Uvicorn con la instancia de la aplicación. uvicorn.run("main:app", host=HOST, port=PORT, reload=True) # English: Entry point for the script, running the main asynchronous function. # Español: Punto de entrada del script, ejecutando la función principal asíncrona. if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: # English: Gracefully shutting down the application on keyboard interrupt. # Español: Apagando la aplicación de manera ordenada al interrumpir con el teclado. print("Shutting down the application...") pass

Rutas

Las rutas son los puntos de acceso a la aplicación. En Lila, las rutas se definen en el directorio routes (por defecto, aunque puedes ubicarlas donde desees) y se importan en main.py para su uso. Las rutas pueden configurarse para manejar solicitudes HTTP, métodos de API y más.

Además de JSONResponse, puedes usar HTMLResponse, RedirectResponse, PlainTextResponse o StreamingResponse para transmitir datos en tiempo real (útil para video/audio en streaming o respuestas grandes).

A continuación, un ejemplo de cómo se definen las rutas en Lila:

routes/api.py

from lila.core.responses import JSONResponse
from lila.core.routing import Router
from lila.core.request import Request

router = Router()

@router.route(path='/api', methods=['GET'])
async def api(request: Request):
    """Función API"""
    return JSONResponse({'api': True})
            

Obtener Parámetros de URL

En esta función recibimos un parámetro a través de la URL usando {param}. Si no se proporciona, se usa 'default'. La respuesta es un JSON con el valor recibido.

Recibiendo Parámetros GET

@router.route(path='/url_with_param/{param}', methods=['GET']) 
async def param(request: Request):  
    param = request.path_params.get('param', 'default') 
    return JSONResponse({"received_param": param})
            

También se puede hacer así:

Recibiendo Parámetros de Consulta GET
 
@router.route(path='/url_with_param/?query_param', methods=['GET','POST']) 
async def query_param(request: Request):  
    query_param = request.query_params.get('query_param', 'query_param')
    return JSONResponse({"received_param": query_param})
            

Uso de Middleware y Decoradores

Los middlewares permiten interceptar las solicitudes antes de que lleguen a la lógica principal de la API. En este ejemplo usamos @validate_token para validar un token JWT en el encabezado.

Validación de Middleware

@router.route(path='/api/token', methods=['GET','POST'])
@validate_token  # Middleware para validar token JWT.
async def api_token(request: Request):
    return JSONResponse({'api': True})
            

Validación de Datos con Pydantic

Pydantic permite definir modelos de datos que validan automáticamente la entrada del usuario. Además, especificar un modelo en la ruta genera documentación automática en /docs.

Validación con Pydantic

from pydantic import EmailStr, BaseModel 

class ExampleModel(BaseModel):
    email: EmailStr  # Valida que sea un email válido.
    password: str  # Cadena de texto para contraseña.

@router.route(path='/api/example', methods=['POST'], model=ExampleModel)
async def login(request: Request):
    body = await request.json()
    try:
        input = ExampleModel(**body)  # Validación automática con Pydantic.
    except Exception as e:
        return JSONResponse({"success": False, "msg": f"JSON inválido: {e}"}, status_code=400) 
    return JSONResponse({"email": input.email, "password": input.password})
            

Generación Automática de Documentación

Gracias a la integración con Pydantic, la documentación de la API se genera automáticamente y es accesible desde /docs. También puedes generar un archivo OpenAPI JSON para herramientas externas.

Generación de Documentación

router.swagger_ui()  # Activa Swagger UI para la documentación de la API.
router.openapi_json()  # Genera OpenAPI JSON para herramientas externas.
            

Importando Rutas en main.py

Para usar las rutas definidas en el router, debes obtenerlas con router.get_routes() e importarlas en main.py.

Importación de Rutas

routes = router.get_routes()  # Obtiene todas las rutas definidas.
            

Prefijos de Rutas y Atajos para Métodos HTTP

Las rutas ahora pueden tener un prefix para agrupar múltiples endpoints bajo un mismo camino. Además, puedes definir rutas directamente con métodos HTTP: get, post, put y delete.

routes/v1_api_example.py

from lila.core.responses import JSONResponse
from lila.core.routing import Router
from lila.core.request import Request
from pydantic import BaseModel

router = Router(prefix="v1/api")

# ---------------- Ejemplo GET ----------------
@router.get("/items/{item_id}")
async def get_item(request: Request):
    """Obtener un ítem por ID."""
    item_id = request.path_params.get("item_id")
    return JSONResponse({"item_id": item_id, "status": "fetched"})

# ---------------- Ejemplo POST ----------------
class CreateItemModel(BaseModel):
    name: str
    description: str

@router.post("/items", model=CreateItemModel)
async def create_item(request: Request):
    """Crear un nuevo ítem."""
    body = await request.json()
    input_data = CreateItemModel(**body)
    return JSONResponse({"name": input_data.name, "description": input_data.description})

# ---------------- Ejemplo PUT ----------------
class UpdateItemModel(BaseModel):
    name: str
    description: str

@router.put("/items/{item_id}", model=UpdateItemModel)
async def update_item(request: Request):
    """Actualizar un ítem existente por ID."""
    item_id = request.path_params.get("item_id")
    body = await request.json()
    input_data = UpdateItemModel(**body)
    return JSONResponse({"item_id": item_id, "updated_name": input_data.name})

# ---------------- Ejemplo DELETE ----------------
@router.delete("/items/{item_id}")
async def delete_item(request: Request):
    """Eliminar un ítem por ID."""
    item_id = request.path_params.get("item_id")
    return JSONResponse({"item_id": item_id, "status": "deleted"})
            

Estos ejemplos muestran cómo:

  • Usar un prefix para versionar endpoints.
  • Usar decoradores específicos por método como @router.get, @router.post, etc.
  • Validar automáticamente la entrada y generar documentación usando modelos Pydantic.
  • Recibir parámetros por URL y cuerpos JSON.
La documentación se genera automáticamente en /docs y /openapi.json.


Archivos Estáticos

Para cargar archivos estáticos (JS, CSS, etc.), puedes usar el método mount().

Los parámetros por defecto son: path: str = '/public', directory: str = 'static', name: str = 'static'

Estático

from lila.core.routing import Router
# Creando una instancia del Enrutador
router = Router()
# Montando los archivos estáticos en la carpeta 'static', con URL '/public' por defecto
router.mount()


Renderizado de Plantillas (Jinja2 )

En Lila, puedes usar Jinja2 para renderizar HTML con datos del servidor

Renderizado con Jinja2

Jinja2 es el motor de plantillas por defecto para renderizar HTML. Puedes pasar datos como traducciones, valores, listas o diccionarios usando el parámetro context.

Uso Básico en Rutas


@router.route(path="/", methods=["GET"])
async def home(request: Request):
  context = {}

  response = render(
    request=request,
    template="index",
    context=context
  )
  # Renderiza la plantilla 'index'

  return response

La Función render

La función render se encuentra en lila.core.templates:


def render(
  request: Request,
  template: str,
  context: dict = {},
  theme_: bool = True,
  translate: bool = True,
  files_translate: list = [],
  lang_default: str = None,
  templates=templates
)

Descripción de Parámetros

  • request:

    The Request object.

  • template (str):

    Template name (without the .html extension).

  • context (dict):

    Data to pass to the template (dictionaries, lists, etc.).

  • theme_ (bool, default=True):

    Applies the default visual theme.

  • translate (bool, default=True):

    Enables the built-in translation system.

  • files_translate (list):

    List of additional translation files.

  • lang_default (str):

    Forced default language (ideal for SEO in routes like /es, /en, etc.).

  • templates:

    Base template collection used by Lila.

In Lila, you can also use translate to pass translations to Jinja2, and parameters like title or version (ideal for structuring APIs: "v1", "v2").

Optimización de Recursos en Producción (Imágenes y CSS)

Imágenes Optimizadas con Jinja2

Lila optimiza automáticamente todas las imágenes para mejorar el rendimiento y el SEO. Al usar la función image() en tus templates, el sistema convierte las imágenes a formato WebP, las redimensiona si es necesario y las comprime sin perder calidad visual.

Uso en HTML (Jinja2)

HTML

<img src="{{ image('img/lila.png') }}" alt="Lila" width="100" height="100" />
      

Qué sucede automáticamente

  • La imagen se convierte a WebP.
  • Si supera los 1920px de ancho, se redimensiona.
  • Se comprime con calidad optimizada.
  • Se guarda en caché para futuros accesos.

Minificación Automática de CSS

La función public() detecta automáticamente si existe una versión minificada de tu archivo CSS. Si no existe, la genera automáticamente para mejorar la velocidad de carga y el SEO.

Uso en HTML (Jinja2)

HTML

<link rel="stylesheet" href="{{ public('css/lila.css') }}" />
      

Comportamiento Automático

  • Si existe lila.min.css, se usa automáticamente.
  • Si no existe, se genera en tiempo real.
  • Los archivos minificados cargan más rápido.

Inyección de Contexto Base

Todo el sistema de optimización funciona gracias al contexto base que se inyecta automáticamente a los templates Jinja2:

Python

def get_base_context(request, files_translate=[], lang_default=None):
    context = {
        "title": TITLE_PROJECT,
        "version": VERSION_PROJECT,
        "lang": lang_default if lang_default else lang(request),
        "translate": t("translations", request, lang_default=lang_default),
        "image": image,
        "static": public,
        "public": public
    }

    for file_name in files_translate:
        context["translate"].update(
            t(file_name, request, lang_default=lang_default)
        )

    return context
      

Beneficios de Rendimiento y SEO

  • ✅ Carga de páginas más rápida
  • ✅ Reducción de peso de archivos
  • ✅ Mejor puntaje en Google Lighthouse
  • ✅ Imágenes automáticas en WebP
  • ✅ Optimización SEO transparente
  • ✅ Sin configuración manual

Gestión de Sesiones

Lila proporciona un sistema de gestión de sesiones simple pero potente construido sobre itsdangerous para datos de sesión firmados criptográficamente. Esto asegura que los datos de sesión no puedan ser manipulados en el lado del cliente.

La clase Session proporciona un conjunto de métodos estáticos para manejar los datos de la sesión.

Establecer Datos de Sesión

El método setSession se utiliza para almacenar datos de sesión en una cookie. Puede manejar cadenas, diccionarios y listas, que se serializan automáticamente a JSON.

ejemplo de setSession

from lila.core.session import Session
from lila.core.responses import JSONResponse

@router.route("/set-session", methods=["GET"])
async def set_session_example(request: Request):
  response = JSONResponse({"message": "La sesión ha sido establecida"})
  session_data = {"user_id": 123, "username": "lila_user"}
  Session.setSession(new_val=session_data, response=response, name_cookie="user_session")
  return response

El método setSession acepta varios parámetros para personalizar la cookie, como secure, samesite, max_age, etc.

Obtener Datos de Sesión

El método getSession recupera los datos de sesión brutos y firmados de una cookie. Para obtener los datos sin firmar y validados, use getSessionValue o unsign.

Desfirmar y Validar Datos de Sesión

Los métodos unsign y getSessionValue obtienen los datos de la sesión en su forma original después de verificar la firma. getSessionValue se recomienda porque también maneja con gracia las firmas caducadas.

ejemplo de getSessionValue

from lila.core.session import Session

@router.route("/get-session", methods=["GET"])
async def get_session_example(request: Request):
  session_data = Session.getSessionValue(request=request, key="user_session")
  if session_data:
    return JSONResponse({"session_data": session_data})
  else:
    return JSONResponse({"message": "No se encontró sesión o la sesión es inválida/caducada."}, status_code=401)

Eliminar una Sesión

El método deleteSession se utiliza para eliminar una cookie de sesión del navegador del cliente.

ejemplo de deleteSession

from lila.core.session import Session
from lila.core.responses import JSONResponse

@router.route("/logout", methods=["GET"])
async def logout(request: Request):
  response = JSONResponse({"message": "Sesión cerrada con éxito"})
  Session.deleteSession(response=response, name_cookie="user_session")
  return response

Referencia Completa de la Clase Session

Aquí está el código completo de la clase Session para su referencia.

core/session.py

import json
from app.config import SECRET_KEY
from itsdangerous import BadSignature, URLSafeTimedSerializer, SignatureExpired
from lila.core.request import Request
from typing import Any, Union, Optional, Dict, List
from lila.core.logger import Logger

serializer = URLSafeTimedSerializer(SECRET_KEY)

class Session:
  @staticmethod
  def setSession(
    new_val: str | dict | list,
    response,
    name_cookie: str = "session",
    secure: bool = True,
    samesite: str = "strict",
    max_age: int = 3600,
    domain: Optional[str] = None,
    http_only: bool = True,
    path: str = "/",
  ) -> bool:
    try:
      if isinstance(new_val, (dict, list)):
        new_val = json.dumps(new_val)
      else:
        new_val = str(new_val)

      signed_session = serializer.dumps(new_val)

      response.set_cookie(
        key=name_cookie,
        value=signed_session,
        max_age=max_age,
        expires=max_age,
        secure=secure,
        httponly=http_only,
        samesite=samesite,
        domain=domain,
        path=path,
      )
      return True
    except (TypeError, ValueError, Exception) as e:
      Logger.error(f"Error setting session: {str(e)}")
      print(f"Error session {str(e)}")
      return False

  @staticmethod
  def getSession(key: str, request: Request) -> Optional[str]:
    return request.cookies.get(key)

  @staticmethod
  def unsign(
    key: str, request: Request, max_age: Optional[int] = None
  ) -> Union[Dict, List, str, None]:
    session_data = request.cookies.get(key)
    if not session_data:
      return None

    try:
      unsigned_data = serializer.loads(session_data, max_age=max_age)
      try:
        return json.loads(unsigned_data)
      except json.JSONDecodeError:
        return unsigned_data

    except BadSignature:
      msg = f"Invalid session signature for cookie: {key}"
      print(msg)
      Logger.warning(msg)
      return None
    except Exception as e:
      msg = f"Error unsigning session: {str(e)}"
      print(msg)
      Logger.error(msg)
      return None

  @staticmethod
  def deleteSession(response, name_cookie: str) -> bool:
    try:
      response.delete_cookie(name_cookie)
      return True
    except Exception as e:
      msg = f"Error deleting session: {str(e)}"
      print(e)
      Logger.error(msg)
      return False

  @staticmethod
  def getSessionValue(
    request: Request, key: str = 'auth', max_age: Optional[int] = 3600
  ) -> Union[Dict, List, str, None]:
    session_data = request.cookies.get(key)
    if not session_data:
      return None

    try:
      unsigned_data = serializer.loads(session_data, max_age=max_age)
      try:
        return json.loads(unsigned_data)
      except json.JSONDecodeError:
        return unsigned_data

    except BadSignature:
      msg = f"Invalid session signature for cookie: {key}"
      print(msg)
      Logger.warning(msg)
      return None
    except SignatureExpired:
      msg = f"Session expired for cookie: {key}"
      print(msg)
      Logger.warning(msg)
      return None
    except Exception as e:
      msg = f"Error unsigning session: {str(e)}"
      print(msg)
      Logger.error(msg)
      return None


Asistente de Carga de Archivos

El asistente upload proporciona una solución completa para manejar la carga de archivos en tu aplicación, incluyendo validación, comprobaciones de seguridad y soporte para traducciones.

Este asistente maneja automáticamente:

  • Validación del método HTTP (solo POST)
  • Validación de Content-Type (multipart/form-data)
  • Validación de la extensión del archivo
  • Límites de tamaño de archivo
  • Detección de archivos vacíos
  • Manejo seguro de nombres de archivo
  • Creación automática de directorios
  • Soporte de traducción para mensajes de error

Parámetros

La función upload acepta estos parámetros:

Parámetros

request: Request          # Objeto de solicitud de Starlette
name_file: str | list     # Nombre del campo para la carga de archivos (defecto: 'file')
UPLOAD_DIR: str           # Directorio para guardar archivos (defecto: 'uploads')
ALLOWED_EXTENSIONS: set   # Extensiones de archivo permitidas (defecto: {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'})
MAX_FILE_SIZE: int        # Tamaño máximo de archivo en bytes (defecto: 10MB)
            

Ejemplo de Uso

Implementación de Ruta

from app.helpers.helpers import upload

@router.route("/upload", methods=["POST"])
async def uploadFile(request: Request):
    response = await upload(request=request, name_file="file")
    return response
                                    

Implementación en el Frontend

HTML + JavaScript

<form onsubmit="upload(event);">
    <fieldset>
        <input type="file" name="file" required>
    </fieldset>
    <button type="submit">
        <i class="icon-check-circle"></i>
        Subir Archivos
    </button>
</form>

<script>
    async function upload(event) {
        event.preventDefault();
        const formElement = event.target;
        const formData = new FormData(formElement);

        try {
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData,
            });

            const result = await response.json();
            if (!response.ok) throw new Error(result.message);

            alert('¡Archivo subido con éxito!');
        } catch (error) {
            alert(error.message);
        }
    }
</script>
                                    

Formato de Respuesta

El asistente devuelve una JSONResponse con esta estructura:

Respuesta Exitosa

{
    "file": "/uploads/filename.ext",
    "success": true,
    "message": "Archivo subido con éxito"
}
                                    
Respuesta de Error

{
    "error": true,
    "success": false,
    "message": "Mensaje de error en el idioma del usuario"
}
                                    

Notas Importantes

Si utilizas windows o el archivo no se crea en el Path recuerda configurar en app/config.py

app/config.py

PATH_UPLOADS = path.join(os.getcwd(), "public", "img", "uploads")

                                    

Markdown /HTML

Para renderizar archivos markdown, se utiliza la función renderMarkdown, que recibe los siguientes parámetros: request, file: str, base_path: str='templates/markdown/', css_files: list=[], js_files: list=[], picocss: bool=False

file es el nombre del archivo Markdown, comenzando desde base_path. Por ejemplo: index.md. Se buscará dentro de templates/markdown/.

css_files y js_files son listas de archivos CSS y JS que se cargarán en el HTML generado.

picocss es un booleano que indica si se debe cargar el archivo de estilos PicoCSS.

Ahora se puede usar layout.html en templates/markdown/ como plantilla base editable:

Ejemplo de un archivo Markdown con soporte bilingüe usando translate:

example.md

# {{ translate["welcome"] }}

Este es un ejemplo simple de **Markdown**. 

## {{ translate["features"] }}
- {{ translate["easy_write"] }}
- {{ translate["convert_html"] }}
- {{ translate["supports_links"] }}

### {{ translate["example_code"] }}

\`\`\`
print("¡Hola, Markdown!")
\`\`\`

Código Python para renderizarlo usando renderMarkdown:

Python

from lila.core.templates import renderMarkdown

@router.route(path='/markdown', methods=['GET'])
async def home(request: Request):
    css = ["/public/css/styles.css"]
    response = renderMarkdown(
        request=request,
        file='example',
        css_files=css,
        picocss=True
    )
    return response


Internacionalización (Traducciones)

Las traducciones se utilizan para internacionalizar una aplicación y mostrar contenido en diferentes idiomas. En Lila, las traducciones se almacenan en el directorio app/locales y pueden cargarse dinámicamente en la aplicación.

Forzar Idioma Predeterminado en las Rutas

El método render acepta un parámetro lang_default para forzar un idioma específico para la plantilla renderizada:

app.routes.py

# Forzar idioma español
@router.route(path="/es", methods=["GET"])
async def home(request: Request):
    response = render(
        request=request,
        template="index",
        lang_default="es"  # Forzar traducciones al español
    )
    return response

# Forzar idioma inglés
@router.route(path="/en", methods=["GET"])
async def home(request: Request):
    response = render(
        request=request,
        template="index",
        lang_default="en"  # Forzar traducciones al inglés
    )
    return response
                                    

Archivos de Traducción

Para cargar un archivo de localización, utiliza la función translate de app.helpers.helpers. Puedes acceder a las traducciones usando:

  • translate - devuelve un diccionario con todas las traducciones
  • translate_ - devuelve una traducción específica (devuelve el texto original si no se encuentra)

Ejemplo de archivo de traducción (app/locales/translations.json):

app/locales/translations.json

{
    "Send": {
        "es": "Enviar",
        "en": "Send"
    },
    "Cancel": {
        "es": "Cancelar",
        "en": "Cancel"
    },
    "Accept": {
        "es": "Aceptar",
        "en": "Accept"
    },
    "Email": {
        "es": "Email",
        "en": "Email"
    },
    "Name": {
        "es": "Nombre",
        "en": "Name"
    },
    "Back": {
        "es": "Volver",
        "en": "Back"
    },
    "Hi": {
        "es": "Hola",
        "en": "Hi"
    }
}
                                    

Uso de Traducciones

Obtener una traducción específica:

example.py

from app.helpers.helpers import translate_

msg_error_login = translate_(
    key="Incorrect email or password", 
    request=request, 
    file_name="guest"
)
                                    

Obtener todas las traducciones de un archivo:

example.py

from app.helpers.helpers import translate

all_translations = translate(
    request=request, 
    file_name="guest"
)
                                    

Modelos (SQLAlchemy)

Los modelos se utilizan para definir la estructura de los datos en la aplicación.

SQLAlchemy como el ORM predeterminado para la gestión de bases de datos. SQLAlchemy permite crear modelos de base de datos, ejecutar consultas y manejar migraciones de manera eficiente.

Uso de Modelos

La clase `Base`, importada de `lila.core.database`, sirve como base para todos los modelos. Los modelos heredan de `Base` para definir tablas de base de datos con SQLAlchemy.

Ejemplo: Modelo de Usuario

Este ejemplo demuestra cómo crear un modelo `User` usando SQLAlchemy. El modelo define una tabla `users` con columnas como `id`, `name`, `email`, `password`, `token`, `active` y `created_at`.
app/models/user.py

        
from sqlalchemy import Table,Column,Integer,String,TIMESTAMP
from lila.core.database import Base
from app.connections import connection

class User(Base):
    __tablename__='users'
    id = Column(Integer, primary_key=True,autoincrement=True)
    name=Column( String(length=50), nullable=False)
    email=Column( String(length=50), unique=True)
    password=Column(String(length=150), nullable=False)
    token=Column(String(length=150), nullable=False)
    active=Column( Integer, nullable=False,default=1)
    created_at=Column( TIMESTAMP)

#Ejemplo de cómo usar SQLAlchemy para hacer consultas a la base de datos
    def get_all(select: str = "id,email,name", limit: int = 1000) -> list:
        query = f"SELECT {select}  FROM users WHERE active =1  LIMIT {limit}"
        result = connection.query(query=query,return_rows=True)#Devuelve filas
        return result 

#Ejemplo de cómo usar SQLAlchemy para hacer consultas a la base de datos
    def get_by_id(id: int, select="id,email,name") -> dict:
        query = f"SELECT {select}  FROM users WHERE id = :id AND active = 1 LIMIT 1"
        params = {"id": id}
        row = connection.query(query=query, params=params,return_row=True)#Devuelve fila
        return row

    #Ejemplo usando abstracción ORM en SQLAlchemy
    @classmethod
    def get_all_orm(cls, db: Session, limit: int = 1000):
        result = db.query(cls).filter(cls.active == 1).limit(limit).all()
        return result


#Ejemplo de cómo usar la clase para hacer consultas a la base de datos
users = User.get_all()
user = User.get_by_id(1)
                    
                    

Para más detalles sobre SQLAlchemy, visita la documentación oficial: Documentación de SQLAlchemy.

Generador de Modelos CLI

Genere automáticamente esqueletos de modelos de SQLAlchemy para agilizar su flujo de trabajo de desarrollo.

El Generador de Modelos CLI es una potente herramienta de línea de comandos que crea plantillas completas de modelos de SQLAlchemy con operaciones CRUD, métodos ORM y ejemplos SQL sin procesar. Ahorra tiempo al proporcionar una estructura consistente para todos sus modelos.

Comandos disponibles

Crear un nuevo modelo
Terminal
python -m cli.model create --name NombreModelo
# o con nombre de tabla personalizado
python -m cli.model create --name Producto --table products
Enumerar todos los modelos
Terminal
python -m cli.model list-models

Estructura del modelo generado

Cada modelo generado incluye columnas básicas (id, name, active, created_at) y métodos CRUD completos. El modelo también incluye comentarios con ejemplos de todos los tipos de columnas disponibles.

app/models/product.py
from sqlalchemy import Column, Integer, String, TIMESTAMP, func
from sqlalchemy.orm import Session, load_only
from core.database import Base
from app.connections import connection

class Product(Base):
    __tablename__ = "products"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(length=100), nullable=False)
    active = Column(Integer, nullable=False, default=1)
    created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())

    @classmethod
    def get_all(cls, select: str = "id,name", limit: int = 1000):
        db = connection.get_session()
        try:
            column_names = [c.strip() for c in select.split(',')]
            columns_to_load = [getattr(cls, c) for c in column_names]
            result = db.query(cls).options(load_only(*columns_to_load)).filter(cls.active == 1).limit(limit).all()
            return [{col: getattr(row, col) for col in column_names} for row in result]
        finally:
            db.close()

    @classmethod
    def get_by_id(cls, db: Session, id: int):
        return db.query(cls).filter(cls.id == id, cls.active == 1).first()

    @classmethod
    def insert(cls, db: Session, params: dict) -> 'Product':
        record = cls(name=params.get("name"), active=params.get("active", 1))
        db.add(record)
        return record

    @classmethod
    def update(cls, db: Session, id: int, params: dict) -> bool:
        record = cls.get_by_id(db, id)
        if record:
            for key, value in params.items():
                if hasattr(record, key):
                    setattr(record, key, value)
            return True
        return False

    @classmethod
    def delete(cls, db: Session, id: int) -> bool:
        record = cls.get_by_id(db, id)
        if record:
            record.active = 0
            return True
        return False

Ejemplo de uso

Ejemplo de uso
# Obtener todos los productos
products = Product.get_all(select="id,name,price", limit=50)

# Obtener producto por ID
db = connection.get_session()
product = Product.get_by_id(db, 1)

# Insertar nuevo producto
new_product = Product.insert(db, {"name": "Laptop", "price": 999.99})
db.commit()

# Actualizar producto
Product.update(db, 1, {"name": "Gaming Laptop"})
db.commit()

# Eliminación temporal
Product.delete(db, 1)
db.commit()
db.close()

Flujo de trabajo completo

  1. Crear el modelo: python -m cli.model create --name Producto
  2. Edita app/models/product.py para agregar columnas personalizadas
  3. Importa el modelo en cli/migrations.py: from app.models.product import Product
  4. Ejecuta migraciones: python -m cli.migrations migrate


  5. Middlewares

    Las funciones de middleware se utilizan para interceptar solicitudes antes de que lleguen a la lógica principal de la aplicación. En Lila, los middlewares se definen en el directorio app/middlewares (se puede modificar a cualquier archivo y/o directorio). Los middlewares se pueden utilizar para tareas como autenticación, registro y manejo de errores.

    Por defecto, Lila viene con 3 middlewares para iniciar cualquier aplicación. Los middlewares se pueden usar con decoradores @my_middleware.

    login_required, para validar que tienes una sesión firmada, para la clave 'auth' que se pasa como parámetro para poder modificarla a tu gusto. Si no se encuentra esta sesión, redirige a la URL que se pasa como parámetro, por defecto, es "/login". De lo contrario, continuará su curso ejecutando la ruta o función.

    Luego tenemos session_active, que se utiliza para verificar si tienes una sesión activa. Redirigirá a la URL que se recibe como parámetro, por defecto, es "/dashboard".

    El tercero es validate_token, que se utiliza para validar un token JWT gracias a los helpers get_token importados en from app.helpers.helpers import get_token.

    app/middlewares/middlewares.py
    
                
    from lila.core.session import Session
    from lila.core.responses import RedirectResponse,JSONResponse
    from lila.core.request import Request
    from functools import wraps
    from app.helpers.helpers import get_token
    
    def login_required(func,key:str='auth',url_return='/login'):
        @wraps(func)
        async def wrapper(request, *args, **kwargs):
            session_data= Session.unsign(key=key,request=request)
            if not session_data:
                return RedirectResponse(url=url_return)
            return await func(request,*args,**kwargs)
        return wrapper
    
    def session_active(func,key:str='auth',url_return:str ='/dashboard'):
        @wraps(func)
        async def wrapper(request,*args,**kwargs):
            session_data= Session.unsign(key=key,request=request)
            if session_data:
                return RedirectResponse(url=url_return)
            return await func(request,*args,**kwargs)
        return wrapper
    
    def validate_token(func):
        @wraps(func)
        async def wrapper(request: Request, *args, **kwargs):
            await check_token(request=request)
            return await func(request, *args, **kwargs)
    
        return wrapper
    
    
    async def check_token(request:Request):
        token = request.headers.get('Authorization')
        if not token:
            return JSONResponse({'session':False,'message': 'Invalid token'},status_code=401)
        
        token = get_token(token=token)
        if isinstance(token,JSONResponse):
            return token
    
                
                

    Aquí te damos varios ejemplos de cómo usar los 3, con los decoradores.
    Middlewares en rutas
                            
    #Middleware para validar el Token JWT.
    @router.route(path='/api/token', methods=['GET','POST'])
    @validate_token #Middleware
    async def api_token(request: Request):
        """Función Api Token"""
        print(get_user_by_id_and_token(request=request))
        return JSONResponse({'api': True})
    
    #Middleware para validar sesión activa
    @router.route(path='/dashboard', methods=['GET'])
    @login_required #Middleware
    async def dashboard(request: Request):
        response = render(request=request, template='dashboard', files_translate=['authenticated'])
        return response
    
    
    #Middleware para validar si el usuario tiene sesión activa (si el usuario tiene sesión redirige a '/dashboard')
    @router.route(path='/login', methods=['GET'])
    @session_active  #Middleware
    async def login(request: Request):
        response = render(request=request, template='auth/login', files_translate=['guest'])
        return response
    
    
                            
                        

    Middleware de Seguridad

    Lila Framework incluye un ErrorHandlerMiddleware incorporado que no solo maneja excepciones no controladas, sino que también proporciona robustas comprobaciones de seguridad para proteger tu aplicación de solicitudes maliciosas. Este middleware está diseñado para bloquear IPs, URLs y rutas sensibles sospechosas, asegurando que tu aplicación permanezca segura.

    Características

    • Bloqueo de IP: Bloquea direcciones IP que han sido marcadas por actividad maliciosa. Las IPs se bloquean por 6 horas por defecto.
    • Bloqueo de URL: Bloquea URLs específicas que se sabe que son maliciosas o sospechosas.
    • Bloqueo de Rutas Sensibles: Bloquea el acceso a rutas sensibles como /etc/passwd, .env, y otras.
    • Bloqueo de Extensiones Maliciosas: Bloquea URLs que contienen extensiones de archivo maliciosas como .php, .asp, .jsp, y .aspx.
    • Detección de HTTP: Bloquea solicitudes que contienen "http" en los parámetros de consulta o en el contenido del cuerpo.

    Configuración

    El middleware utiliza tres archivos JSON ubicados en el directorio app/security:

    • blocked_ips.json: Almacena IPs bloqueadas con su tiempo de expiración.
    • blocked_urls.json: Almacena URLs bloqueadas con su tiempo de expiración.
    • sensitive_paths.json: Almacena una lista de rutas sensibles para bloquear.

    Si estos archivos no existen, se crean automáticamente y se inicializan con valores predeterminados:

    security/sensitive_paths.json
    
    [
        "/etc/passwd",
        ".env",
        "wp-content",
        "docker/.env",
        "owa/auth/logon.aspx",
        "containers/json",
        "models",
        "autodiscover/autodiscover.json",
        "heapdump",
        "actuator/heapdump",
        "cgi-bin/vitogate.cgi",
        "CFIDE/wizards/common/utils.cfc",
        "var/www/html/.env",
        "home/user/.muttrc",
        "usr/local/spool/mail/root",
        "etc/postfix/master.cf"
    ]
                                        

    Uso

    El ErrorHandlerMiddleware se aplica automáticamente a todas las solicitudes. Puedes personalizar su comportamiento modificando los archivos JSON en el directorio security.

    core/middleware.py
    
    from starlette.middleware.base import BaseHTTPMiddleware
    from lila.core.responses import JSONResponse, HTMLResponse
    from lila.core.request import Request
    from lila.core.logger import Logger
    from datetime import datetime, timedelta
    import json
    import os
    
    def load_blocked_data(file_path, default_value):
    try:
    if not os.path.exists(file_path):
        with open(file_path, "w") as file:
            json.dump(default_value, file, indent=4)
        return default_value
    
    with open(file_path, "r") as file:
        content = file.read().strip()
        if not content:
            with open(file_path, "w") as file:
                json.dump(default_value, file, indent=4)
            return default_value
    
        try:
            return json.loads(content)
        except json.JSONDecodeError:
            with open(file_path, "w") as file:
                json.dump(default_value, file, indent=4)
            return default_value
    
    except Exception as e:
    Logger.error(f"Error loading {file_path}: {str(e)}")
    return default_value
    
    def save_blocked_data(file_path, data):
    try:
    with open(file_path, "w") as file:
        json.dump(data, file, indent=4)
    except Exception as e:
    Logger.error(f"Error saving {file_path}: {str(e)}")
    
    async def is_blocked(blocked_data, key, request: Request):
    if key in blocked_data:
    expiration_time = datetime.fromisoformat(blocked_data[key]["expiration_time"])
    if datetime.now() < expiration_time:
        req = await Logger.request(request=request)
        Logger.warning(f"Blocked: {key} \n {req}")
        return True
    return False
    
    class ErrorHandlerMiddleware(BaseHTTPMiddleware):
    def __init__(
    self,
    app,
    blocked_ips_file="security/blocked_ips.json",
    blocked_urls_file="security/blocked_urls.json",
    sensitive_paths_file="security/sensitive_paths.json",
    ):
    super().__init__(app)
    self.blocked_ips_file = blocked_ips_file
    self.blocked_urls_file = blocked_urls_file
    self.sensitive_paths_file = sensitive_paths_file
    
    self.blocked_ips = load_blocked_data(blocked_ips_file, default_value={})
    self.blocked_urls = load_blocked_data(blocked_urls_file, default_value={})
    self.sensitive_paths = load_blocked_data(sensitive_paths_file, default_value=[])
    
    async def dispatch(self, request, call_next):
    try:
        client_ip = request.client.host
        url_path = request.url.path
        query_params = str(request.query_params)
        body = await request.body()
    
        if await is_blocked(self.blocked_ips, client_ip, request=request):
            return HTMLResponse(
                content="

    Access Denied

    Your IP has been temporarily blocked.

    ", status_code=403, ) if await is_blocked(self.blocked_urls, url_path, request=request): return HTMLResponse( content="

    Access Denied

    This URL has been temporarily blocked.

    ", status_code=403, ) malicious_extensions = [".php", ".asp", ".jsp", ".aspx"] if any(ext in url_path for ext in malicious_extensions): self.blocked_ips[client_ip] = { "expiration_time": (datetime.now() + timedelta(hours=6)).isoformat() } save_blocked_data(self.blocked_ips_file, self.blocked_ips) return HTMLResponse( content="

    Access Denied

    Malicious URL detected.

    ", status_code=403, ) if "http" in query_params or "http" in str(body): self.blocked_ips[client_ip] = { "expiration_time": (datetime.now() + timedelta(hours=6)).isoformat() } save_blocked_data(self.blocked_ips_file, self.blocked_ips) return HTMLResponse( content="

    Access Denied

    Malicious query parameters detected.

    ", status_code=403, ) if any(path in url_path or path in str(body) for path in self.sensitive_paths): self.blocked_ips[client_ip] = { "expiration_time": (datetime.now() + timedelta(hours=6)).isoformat() } save_blocked_data(self.blocked_ips_file, self.blocked_ips) return HTMLResponse( content="

    Access Denied

    Sensitive path detected.

    ", status_code=403, ) Logger.info(await Logger.request(request=request)) response = await call_next(request) return response except Exception as e: Logger.error(f"Unhandled Error: {str(e)}") return JSONResponse( {"error": "Internal Server Error", "success": False}, status_code=500 )

    Referencia de Comandos CLI

    Lila Framework proporciona potentes herramientas CLI para acelerar el desarrollo. Todos los comandos siguen el patrón python -m cli.{comando}

    📦 Generador de Modelos

    Crea modelos SQLAlchemy con métodos CRUD automáticamente.

    cli.model
    
    # Crear un nuevo modelo
    python -m cli.model create --name Product
    
    # Crear con nombre de tabla personalizado
    python -m cli.model create --name Product --table products
    
    # Listar todos los modelos
    python -m cli.model list-models
    
    

    🏗️ Generador de Scaffold

    Genera funcionalidad CRUD completa desde modelos existentes.

    cli.scaffold_crud
    
    # Generar scaffold desde modelo
    python -m cli.scaffold_crud --model Product
    
    # Nombre de ruta personalizado
    python -m cli.scaffold_crud --model Product --name products
    
    

    🗄️ Migraciones

    Ejecuta migraciones de base de datos para crear tablas desde modelos.

    cli.migrations
    
    # Ejecutar migraciones
    python -m cli.migrations migrate
    
    # Refrescar (eliminar y recrear todas las tablas)
    python -m cli.migrations migrate --refresh
    
    

    🔐 Autenticación

    Genera sistema de autenticación con login, registro y recuperación de contraseña.

    cli.auth
    
    # Generar sistema de autenticación
    python -m cli.auth main
    
    

    👤 Panel de Administración

    Crea usuarios administradores y panel de administración.

    cli.create_admin
    
    # Crear usuario administrador
    python -m cli.create_admin --password mypassword
    
    # Crear panel de administración
    python -m cli.create_panel_admin --password mypassword
    
    

    ⚡ Minificar Assets

    Minifica archivos CSS y JavaScript para producción.

    cli.minify
    
    # Minificar todos los archivos CSS y JS
    python -m cli.minify
    
    

    ⚛️ Integración con React

    Configura entorno de desarrollo React con backend Lila.

    cli.react
    
    # Instalar React y configurar en el proyecto
    python -m cli.react create
    
    # Iniciar servidor de desarrollo React
    python -m cli.react dev
    
    

    Conexiones a la base de datos

    Para usar conexiones, necesitas importar la clase Database de lila.core.database. Con eso puedes conectarte a tu base de datos, que puede ser SQLite, MySQL, PostgreSQL o lo que quieras configurar.

    A continuación te dejamos el ejemplo de cómo conectarte. La conexión se cerrará automáticamente después de ser utilizada, por lo que puedes usarla como en este ejemplo en la variable connection

    app/connections.py
    
                
    from lila.core.database import Database
    
    #SQLite
    
    #Ejemplo de conexión a una base de datos sqlite
    config = {"type":"sqlite","database":"test"} #test.db
    connection = Database(config=config)
    connection.connect()
    
    #MySql
    
    #Ejemplo de conexión a una base de datos mysql
    config = {"type":"mysql","host":"127.0.0.1","user":"root","password":"password","database":"db_test","auto_commit":True}
    connection = Database(config=config)
    connection.connect()
    mysql_connection = connection
    
    
                    
                    

    Migraciones

    En Lila Framework, las migraciones de bases de datos se pueden gestionar usando SQLAlchemy y la configuración de Lila para hacer las migraciones lo más fácil posible. El framework ahora soporta migraciones por línea de comandos a través de Typer, proporcionando una forma más intuitiva y flexible de gestionar tu esquema de base de datos.


    Métodos de Migración

    Hay dos formas principales de definir las tablas de la base de datos:

    1. Usando Table

    Este método define manualmente la estructura de la tabla usando el objeto Table de SQLAlchemy.

    cli/migrations.py
    
    from sqlalchemy import Table, Column, Integer, String, TIMESTAMP
    from app.connections import connection
    import typer
    import asyncio
    
    app = typer.Typer()
    
    # Ejemplo de creación de migraciones para 'users'
    table_users = Table(
        'users', connection.metadata,
        Column('id', Integer, primary_key=True, autoincrement=True),
        Column('name', String(length=50), nullable=False),
        Column('email', String(length=50), unique=True),
        Column('password', String(length=150), nullable=False),
        Column('token', String(length=150), nullable=False),
        Column('active', Integer, default=1, nullable=False),
        Column('created_at', TIMESTAMP),
    )
    
    async def migrate_async(connection, refresh: bool = False) -> bool:
        try:
            if refresh:
                connection.metadata.drop_all(connection.engine)
            connection.prepare_migrate([table_users])  # para tablas
            connection.migrate()
            print("Migraciones completadas")
            return True
        except RuntimeError as e:
            print(e)
            return False
    
    @app.command()
    def migrate(refresh: bool = False):
        """Ejecutar migraciones de base de datos"""
        success = asyncio.run(migrate_async(connection, refresh))
        if not success:
            raise typer.Exit(code=1)
    
    if __name__ == "__main__":
        app()
                                        

    2. Usando Modelos (Recomendado)

    Este enfoque define las tablas de la base de datos como clases de Python que heredan de "Base". Este es el método recomendado ya que proporciona más estructura y capacidades ORM.

    app/models/user.py
    
    from lila.core.database import Base
    from sqlalchemy import Column, Integer, String, TIMESTAMP
    
    class User(Base):
        __tablename__ = 'users'
        id = Column(Integer, primary_key=True, autoincrement=True)
        name = Column(String(length=50), nullable=False)
        email = Column(String(length=50), unique=True)
        password = Column(String(length=150), nullable=False)
        token = Column(String(length=150), nullable=False)
        active = Column(Integer, nullable=False, default=1)
        created_at = Column(TIMESTAMP)
                                        
    cli/migrations.py (Versión de Modelos)
    
    from app.connections import connection
    from lila.core.database import Base  # Importar Base para migraciones en modelos
    from app.models.user import User  # Importar modelos para migraciones
    import typer
    import asyncio
    
    app = typer.Typer()
    
    async def migrate_async(connection, refresh: bool = False) -> bool:
        try:
            if refresh:
                connection.metadata.drop_all(connection.engine)
            connection.migrate(use_base=True)  # para modelos
            print("Migraciones completadas")
            return True
        except RuntimeError as e:
            print(e)
            return False
    
    @app.command()
    def migrate(refresh: bool = False):
        """Ejecutar migraciones de base de datos"""
        success = asyncio.run(migrate_async(connection, refresh))
        if not success:
            raise typer.Exit(code=1)
    
    if __name__ == "__main__":
        app()
                                        

    Ejecutando Migraciones

    Para ejecutar las migraciones, usa el siguiente comando en tu código:

    Comando de Terminal
    
    # Migración básica
    python -m cli.migrations 
    
    # Refrescar todas las tablas (eliminar y recrear)
    python -m cli.migrations --refresh
                                        

    Opciones de Comando

    • migrate: Ejecuta las migraciones de la base de datos
    • --refresh: Flag opcional para eliminar y recrear todas las tablas

    Nota: Cuando uses Modelos, asegúrate de importar todas tus clases de modelos en el archivo de migraciones para que SQLAlchemy pueda detectarlos para las migraciones.


    Auth Scaffold

    En Lila Framework, ahora puedes generar automáticamente todas las rutas, templates, modelos y middlewares de autenticación con un solo comando. Esto te permite configurar login, registro, recuperación de contraseña y dashboard sin crear archivos a mano.


    Características

    • Genera rutas: /login, /register, /forgot-password, /invalid-token, /change-password
    • Crea las páginas de dashboard y profile
    • Incluye middlewares, sesiones y validaciones usando Pydantic
    • Actualiza automáticamente main.py y cli/migrations.py

    Ejecutando el Auth Scaffold

    Usa el siguiente comando en tu terminal:

    Comando Terminal
    
    # Ejecuta el scaffold de auth para generar rutas, templates, modelos y middlewares
    python -m cli.auth
          

    Archivos y Estructura Generados

    El scaffold creará/actualizará:

    • Templates: login, register, forgot-password, change-password
    • Modelos: actualiza User y crea AuthModel
    • Rutas y funciones: manejadores para cada template/acción
    • Middlewares: gestión de sesiones y validaciones

    Generador de Scaffold CLI

    Genera funcionalidad CRUD completa desde modelos existentes con un solo comando. El generador de scaffold crea rutas, plantillas y automáticamente importa todo en tu aplicación.

    Uso Básico

    Primero, crea tu modelo usando el CLI de modelos, luego genera el scaffold:

    Generador de Scaffold
    
    # Crear un modelo primero
    python -m cli.model create --name Product
    
     
    
    # Generar scaffold CRUD completo
    python -m cli.scaffold_crud --model Product
    
    # Nombre de ruta personalizado
    python -m cli.scaffold_crud --model Product --name products
    
    #Ejecutar migraciones con refresh
    python -m cli.migrations --refresh
    
    

    Qué se Genera

    El generador de scaffold crea un sistema CRUD completo:

    • app/routes/{name}.py - Rutas CRUD completas
    • templates/html/{name}/index.html - DataTable con modales
    • Auto-importa rutas en main.py

    Endpoints Generados

    Las siguientes rutas se crean automáticamente:

    Endpoints API
    
    GET  /product              → Página HTML con DataTable
    GET  /api/product          → Lista JSON de todos los productos
    GET  /api/product/{id}     → Producto individual JSON
    POST /api/product          → Crear nuevo producto
    PUT  /api/product/{id}     → Actualizar producto
    DELETE /api/product/{id}   → Borrado lógico (active=0)
    
    

    Características

    • ✅ Valida que el modelo exista antes de generar
    • ✅ DataTable responsive con búsqueda y paginación
    • ✅ Modales de Crear y Editar
    • ✅ Funcionalidad de borrado lógico (active=0)
    • ✅ Ejemplos de validación Pydantic (comentados)
    • ✅ Soporte multilenguaje
    • ✅ Auto-importación en main.py

    Personalización

    Después de la generación, puedes personalizar:

    • Importa en main.py la ruta generada
    • Edita app/routes/{name}.py para agregar lógica de negocio
    • Descomenta los modelos Pydantic para validación
    • Personaliza la plantilla HTML
    • Agrega middleware personalizado

    Integración de Islas React

    Lila ofrece una poderosa arquitectura de "Islas React", permitiéndote incrustar componentes interactivos de React directamente en tus plantillas Jinja2. Este enfoque combina lo mejor de ambos mundos: el SEO y el rendimiento del renderizado del lado del servidor con Lila, y la interactividad de React.

    Instalación

    Para configurar el entorno de React en tu proyecto desde el directorio raíz, ejecuta:

    Comando CLI
    
    python -m cli.react create
                

    Esto generará el esquema de package.json, vite.config.js y la estructura del directorio react/.

    Uso

    Una vez instalado, puedes usar los nuevos ayudantes de Jinja2 vite_assets() y react().

    • vite_assets(): Inyecta las etiquetas de script necesarias. En modo DEBUG=True, se conecta al servidor de desarrollo de Vite (HMR). En producción, carga los activos optimizados desde public/build.
    • react(component, props): Monta un componente de React específico.

    Ejemplo de una plantilla HTML completa:

    templates/html/react.html
    
    <!DOCTYPE html>
    <html lang="{{ lang }}">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="color-scheme" content="light dark" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <meta name="description" content="{{ description }}" />
        <meta name="keywords" content="{{ keywords }}" />
        <meta name="author" content="{{ author }}" />
        <title>{{ title }}</title>
        <link rel="icon" type="image/x-icon" href="{{ public('img/lila.png') }}" />
        <link rel="stylesheet" href="{{ public('css/lila.css') }}" />
        {{ vite_assets() | safe }}
      </head>
      <body>
        <main class="mt-4 container">
          <h1>Ejemplo de integración React</h1>
          <div>{{ react('Counter', {'start': 3}) | safe }}</div>
        </main>
      </body>
    </html>
                
    Cómo Funciona

    Desarrollo (DEBUG=True):
    El ayudante vite_assets() inyecta scripts apuntando a http://localhost:5173. Puedes iniciar el servidor de desarrollo con:

    Terminal
    
    npm run dev
    # O
    python -m cli.react dev
                

    Producción (DEBUG=False):
    Ejecuta npm run build para compilar tu código React en public/build. El ayudante detectará automáticamente el archivo manifest.json y cargará los archivos CSS y JS hasheados para un rendimiento óptimo.

    Beneficios
    • SEO con Jinja: El contenido de tu página principal es renderizado por Jinja, asegurando que los motores de búsqueda puedan leerlo perfectamente.
    • Interactividad React: Usa React solo donde sea necesario (arquitectura de Islas).
    • Sesiones Seguras: Puedes confiar en las sesiones seguras y firmadas de Lila (itsdangerous) para la autenticación, mientras los componentes de React se comunican con tus puntos finales de API.

    React Render (componentes de renderizado con htmlResponse sin plantillas jinja)

    Este método reactRender se utiliza para devolver el HTML directamente y cargar una página o componente de React con estilos, metaetiquetas, scripts personalizados y contenido/props.

    ReactRender
    
    from lila.core.templates import render,renderMarkdown,renderReact
    ...
    router = Router()
    @router.get("/react-page")
    async def react_page(request: Request):
        response = renderReact(component="Counter",
        props={
            "start": 5
        } ,
        options={
            "title": "React Page",
            "meta": [{
              "name": "description",
              "content": "A React page with a counter component"
            }],
            "styles": ["/public/css/lila.css"]
        }                      
        )
        return response
                

    Generación Simple de CRUD de API REST

    En Lila, tenemos una forma sencilla de generar CRUDs con documentación automática, permitiéndote crear tu API REST de manera eficiente.

    Gracias a la combinación de modelos de SQLAlchemy y Pydantic, es posible realizar validaciones de datos y ejecutar consultas estructuradas para la generación de la API.

    Además, puedes integrar middlewares personalizados para validar tokens, gestionar sesiones o procesar solicitudes. Con solo unas pocas líneas de código, puedes generar un CRUD de API REST completamente documentado.


    Si aún no lo has hecho, habilita las migraciones cuando inicies el servidor. Por defecto, utiliza SQLite, creará un archivo de base de datos lila.sqlite en la raíz del proyecto.

    term
    
    python -m cli.migrations 
    
    
    

    routes/api.py
    
    from lila.core.request import Request
    from lila.core.responses import JSONResponse
    from lila.core.routing import Router
    from pydantic import EmailStr, BaseModel
    from app.middlewares.middlewares import validate_token, check_token, check_session
    from app.connections import connection  # Conexión a la base de datos con SQLAlchemy
    from app.models.user import User  # Modelo 'User' de SQLAlchemy
    
    router = Router()# Inicializa la instancia del enrutador para gestionar las rutas de la API.
    
    
    # Modelo Pydantic para validaciones al crear o modificar un usuario.
    class UserModel(BaseModel):
        email: EmailStr
        name: str
        token: str
        password: str
    
    # Definiciones de middlewares para operaciones CRUD
    middlewares_user = {
        "get": [],
        "post": [], 
        "get_id": [],
        "put": [],
        "delete": [check_session, check_token],#Ejemplo de middlewares para sesión web 'check_session' y jwt 'check_token'
    }
    
    # Generar automáticamente CRUD con validaciones y configuraciones
    router.rest_crud_generate(
        connection=connection,  # Conexión a la base de datos
        model_sql=User,  # Modelo SQLAlchemy
        model_pydantic=UserModel,  # Modelo Pydantic
        select=["name", "email", "id", "created_at", "active"],  # Campos a seleccionar en las consultas
        delete_logic=True,  # Habilita el borrado lógico (actualiza 'active = 0' en lugar de eliminar registros)
        active=True,  # Filtra automáticamente los registros activos ('active = 1')
        middlewares=middlewares_user,  # Middlewares personalizados para cada acción CRUD
    )
                                

    Puedes crear tus propios middlewares y pasarlos como una lista para personalizar la seguridad y las validaciones para cada operación de rest_crud_generate.

    Para generar la documentación, recuerda siempre ejecutarla después de las rutas router.swagger_ui() y router.openapi_json()

    Parámetros de la Función para rest_crud_generate

    A continuación se muestran los parámetros que acepta esta función para generar CRUD automáticamente:

    core/routing.py
    
    def rest_crud_generate(
        self,
        connection,
        model_sql,
        model_pydantic: Type[BaseModel],
        select: Optional[List[str]] = None,
        columns: Optional[List[str]] = None,
        active: bool = False,
        delete_logic: bool = False,
        middlewares: dict = None,
        jsonresponse_prefix:str='',#Devolver con prefijo de lista o dict 'data' como primera clave
    
        user_id_session:bool| str=False #Ejemplo 'user id' para validar en la consulta con where 'user_id'= id session_user
    
    ) :                    
    Documentación Automática

    A continuación se muestra un ejemplo de la documentación generada para la función rest_crud_generate:

    Ve a http://127.0.0.1:8000/docs, o como esté configurado en tu archivo .env (por HOST y PORT).

    GET - Obtener todos los usuarios
    GET - Obtener todos los usuarios
    GET - Salida de todos los usuarios
    GET - Salida
    POST - Crear nuevo usuario
    POST - Crear nuevo usuario
    GET_ID - Obtener un usuario específico
    GET_ID - Obtener un usuario específico
    PUT - Actualizar usuario
    PUT - Actualizar usuario
    DELETE - Eliminar usuario
    DELETE - Eliminar usuario

    En este ejemplo, lo hicimos con 'users', pero puedes aplicarlo como quieras según tu lógica, 'products', 'stores', etc. Incluso modificando el núcleo en core/routing.py.


    Para la función de método 'POST' o 'PUT', Si el framework detecta que pasas datos en el cuerpo de la solicitud como: 'password', lo codificará automáticamente con argon2 para hacerlo seguro. Ejemplo de cuerpo:

    
    {
    "email":"example@example.com",
    "name":"name",
    "password":"my_password_secret"
    }
                            
                            

    Luego, si pasas 'token' o 'hash', con la función helper generate_token_value , genera automáticamente un token, que se guardará en la base de datos como la columna 'token' con el valor generado por la función. Ejemplo de cuerpo:

    
    {
    "email":"example@example.com",
    "name":"name",
    "password":"my_password_secret",
    "token":""
    }
                            
                            

    Con 'created_at' o 'created_date', guardará la fecha y hora del momento, siempre que ese campo exista en la tabla de la base de datos. Ejemplo de cuerpo:

    
    {
    "email":"example@example.com",
    "name":"name",
    "password":"my_password_secret",
    "token":"",
    "created_at":""
    }
                            
                            

    Para los métodos 'PUT', 'GET' (get_id) o 'DELETE' Es opcional según la lógica de cada API REST, puedes pasarlo como query string, user_id o id_user, un ejemplo sería GET, PUT o DELETE como método a la url http://127.0.0.1:8000/api/products/1?user_id=20

    Donde valida que el ID del producto '1' existe pero también que pertenece al ID de usuario '20'.

    Nuevos Parámetros para Generación de CRUD en HTML

    Además de generar el CRUD de la API REST, rest_crud_generate ahora acepta tres nuevos parámetros para crear un CRUD en HTML completamente funcional:

    • generate_html: bool = True – Genera automáticamente un CRUD en HTML usando un datatable responsive de Lila. Útil para generar la interfaz solo en desarrollo.
    • rewrite_template: bool = False – Si es True, sobrescribe templates existentes.
    • url_html: str = None – Opcional. Permite especificar la URL de salida del HTML. Si no se provee, el HTML puede copiarse y usarse en cualquier ruta.

    Esta funcionalidad permite generar CRUDs con create, update, delete, paginación, validaciones y utiliza el mismo REST CRUD API como backend. Es ideal para crear rápidamente múltiples CRUDs en HTML y backend para tu aplicación: con aproximadamente 30 líneas de código, ya tienes un CRUD en HTML funcional listo para pequeños ajustes o incluso listo para usar tal cual.

    Puedes pasar DEBUG desde from app.config import DEBUG a generate_html para que solo se genere en desarrollo. Si pasas generate_html=False, el HTML no se generará automáticamente y puedes usarlo como desees.


    Panel de Administración

    El módulo Admin te permite gestionar un panel de administración para tu aplicación. Incluye autenticación, gestión de modelos, métricas del sistema y más. Este panel es altamente personalizable y se integra fácilmente con tu aplicación.

    El panel de administración ahora es más modular y flexible. Todos los componentes relacionados con la administración (plantillas, rutas y configuración) se encuentran en la carpeta admin, lo que facilita su personalización y extensión.

    Características Clave

    • Autenticación: Inicio y cierre de sesión seguros para administradores.
    • Gestión de Modelos: Genera automáticamente rutas y vistas para gestionar tus modelos.
    • Métricas del Sistema: Supervisa el uso de memoria y CPU tanto para la aplicación como para el servidor.
    • Gestión de Contraseñas: Permite a los administradores cambiar sus contraseñas.
    • Registros: Permite a los administradores ver los Logs de la aplicación.

    Uso Básico

    Para usar el panel de administración, necesitas importar la clase Admin de admin.routes y pasarle una lista opcional de modelos que quieres gestionar. Cada modelo debe implementar un método get_all para ser mostrado en el panel de administración.

    main.py
                    
    # English: Here we activate the admin panel with default settings.
    # Español: Aquí activamos el panel de administrador con configuraciones predeterminadas.
    from app.routes.admin import Admin
    from app.models.user import User
    admin_routes=Admin(models=[User])
    all_routes = list(itertools.chain(routes, api_routes,admin_routes))
                    
                

    Configuración y Migraciones

    Antes de usar el panel de administración, necesitas ejecutar las migraciones:

    Terminal
                    
    python -m cli.migrations
                    
                

    Creación de Usuarios Administradores

    Los usuarios administradores se pueden crear a través de la línea de comandos con parámetros personalizables:

    Terminal
                    
    # Uso predeterminado (se genera una contraseña aleatoria)
    python -m cli.create_panel_admin
    
    # Con nombre de usuario y contraseña personalizados
    python -m cli.create_panel_admin --user miadmin --password misecretopassword
                    
                

    Rutas Generadas

    El panel de administración genera automáticamente las siguientes rutas:

    Puedes acceder a las plantillas en la carpeta templates/html/admin.

    • Login: /admin/login (GET/POST)
    • Logout: /admin/logout (GET)
    • Dashboard: /admin (GET)
    • Gestión de Modelos: /admin/{model_plural} (GET)

    Middleware de Autenticación

    Para proteger las rutas y asegurar que solo los administradores autenticados puedan acceder a ellas, usa el decorador @admin_required de core/admin.py.

    Uso del Middleware
                    
    from lila.core.admin import admin_required
    
    @router.route(path="/admin", methods=["GET"])
    @admin_required
    async def admin_route(request: Request):
        menu_html = menu(models=models)
        return await admin_dashboard(request=request, menu=menu_html)
                    
                

    Métricas del Sistema

    El panel de administración muestra métricas en tiempo real, incluyendo:

    • Uso de memoria del framework Lila.
    • Uso de CPU del framework Lila.
    • Uso de memoria del sistema.
    • Uso de CPU del sistema.

    Estas métricas se actualizan cada 10 segundos.

    Registros

    En Lila, usamos un middleware que puedes habilitar o deshabilitar si quieres usar Logs para información, advertencias o errores en tu aplicación.

    El middleware se encuentra en core/middleware.py y se agrega a la aplicación con: app.add_middleware(ErrorHandlerMiddleware).

    Esto ayuda a generar registros que puedes ver en el panel de administración, organizados por fecha (en carpetas) y tipo.

    admin


    admin

    Creación de Usuarios Administradores (Comando Simple)

    Además del comando para crear el panel completo, ahora puedes crear administradores individuales sin afectar las rutas, middlewares o configuraciones existentes. Este comando es útil para añadir nuevos usuarios administradores de manera rápida.

    Terminal
    
    # Crear un administrador con nombre de usuario y contraseña específicos
    python -m cli.create_admin --username miadmin --password misecretopassword
    
    # Crear un administrador con usuario personalizado y contraseña generada automáticamente
    python -m cli.create_admin --username miadmin
    
            

    Nota: Este comando solo agrega el usuario administrador en la base de datos. No genera rutas, middlewares ni plantillas, por lo que se puede usar incluso si el panel ya está activo o personalizado.


    Minificar archivos para producción

    Con el comando lila-minify puedes minificar tus archivos js, css , que se encuentran en las carpetas public y templates.

    Comando de Terminal
    
    lila-minify
    #O
    python -m cli.minify
    

    Despliegue a Producción

    Servidor Linux (con systemd)

    Para desplegar tu aplicación Lila en un servidor Linux, se recomienda usar un entorno virtual y un servicio systemd para gestionar el proceso.

    1. Crear un Entorno Virtual

    Terminal
    
    python -m venv venv
    source venv/bin/activate
    pip install -r requirements.txt
                            

    2. Crear un Script de Inicio

    start_lila.sh
    
    #!/bin/bash
    source /ruta/a/tu/proyecto/venv/bin/activate
    python /ruta/a/tu/proyecto/main.py
                            

    3. Crear un Servicio systemd

    /etc/systemd/system/lila.service
    
    [Unit]
    Description=Servicio de Lila Framework
    After=network.target
    
    [Service]
    User=tu_usuario
    Group=tu_grupo
    WorkingDirectory=/ruta/a/tu/proyecto
    ExecStart=/ruta/a/tu/proyecto/start_lila.sh
    Restart=always
    RestartSec=10
    
    [Install]
    WantedBy=multi-user.target
                            

    4. Habilitar e Iniciar el Servicio

    Terminal
    
    sudo systemctl daemon-reload
    sudo systemctl enable lila.service
    sudo systemctl start lila.service
    chmod +x /path/your_project/start_lila.sh
                            

    Servidor Windows

    En Windows, puedes usar el Programador de Tareas para ejecutar tu aplicación al inicio.

    1. Crea un script `.bat` o `.ps1` para iniciar tu aplicación.
    2. Abre el Programador de Tareas y crea una nueva tarea que se ejecute al inicio del sistema.
    3. Establece la acción para que ejecute tu script.

    Desactivar el Modo de Depuración

    En tu archivo `.env`, asegúrate de establecer `DEBUG=False` para producción.

    .env
    
    DEBUG=False