Implementación de un framework web en Python usando Werkzeug

Arquitectura del Framework

Este proyecto es una implementación educativa de un framework web en Python, inspirada en Flask y basada en la librería Werkzeug. Su propósito principal es comprender los fundamentos del procesamiento de solicitudes HTTP mediante WSGI.

WSGI (Web Server Gateway Interface) define un estándar para la comunicación entre servidores web y aplicaciones Python. Una aplicación WSGI debe ser invocable como una función que acepte dos argumentos: environ (un diccionario con información sobre la solicitud) y start_response (una función para iniciar la respuesta).

Clase Application

class Application:
    def __init__(self, template_path='templates', static_folder='static', root_path=None):
        self.template_path = template_path
        self.static_folder = static_folder
        self.root_path = root_path or os.getcwd()
        self.url_map = Map()
        self.view_functions = {}
        self.before_request_funcs = []
        self.after_response_funcs = []

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        ctx.push()
        try:
            response = self.dispatch_request(environ)
            return response(environ, start_response)
        finally:
            ctx.pop()

El método wsgi_app maneja el ciclo completo de solicitud-respuesta. Crea un contexto de solicitud, lo empuja a una pila local, ejecuta la lógica de enrutamiento y finalmente limpia el contexto.

Enrutamiento con Map y Rule

Werkzeug proporciona clases robustas para el enrutamiento. Se utiliza Map para almacenar reglas y Rule para definir patrones de URL.

def route(self, rule, endpoint=None, **options):
    def decorator(f):
        self.add_url_rule(rule, f, endpoint, **options)
        return f
    return decorator

def add_url_rule(self, rule, view_func=None, endpoint=None, **options):
    endpoint = endpoint or view_func.__name__
    if endpoint in self.view_functions:
        raise ValueError(f"Endpoint '{endpoint}' already exists.")
    rule_obj = Rule(rule, endpoint=endpoint, **options)
    self.url_map.add(rule_obj)
    if view_func:
        self.view_functions[endpoint] = view_func

Al registrar una ruta, se crea un mapeo entre el nombre del endpoint y la función asociada. La coincidencia de rutas se realiza mediante:

adapter = self.url_map.bind_to_environ(environ)
rv = adapter.dispatch(lambda e, v: self.view_functions[e](**v))

Gestión de Excepciones

Se aprovecha la funcionalidad integrada de Werkzeug para manejar errores HTTP. El método abort() permite lanzar excepciones con códigos HTTP específicos.

def abort(code, message=None):
    exc_class = HTTPException.mapping.get(code, InternalServerError)
    raise exc_class(message)

Además, se permite personalizar el comportamiento mediante un diccionario de excepciones y funciones de manejo.

Servicio de Archivos Estáticos

Los archivos estáticos se sirven directamente desde una carpeta especificada durante la inicialización.

def send_static_file(path):
    full_path = os.path.join(current_app.static_folder, path)
    if not os.path.isfile(full_path):
        raise NotFound()
    with open(full_path, 'rb') as f:
        data = wrap_file(request.environ, f)
    return Response(data, mimetype='application/octet-stream', direct_passthrough=True)

La función wrap_file gestioan el flujo binario de forma eficiente, permitiendo el envío incremental de datos.

Contextso: Solicitud y Aplicación

Se implementan contextos separados para mantener estado por hilo, evitando interferencias entre solicitudes concurrentes.

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

request = LocalProxy(partial(get_request_obj, 'request'))
session = LocalProxy(partial(get_request_obj, 'session'))
g = LocalProxy(partial(get_app_ctx_obj, 'g'))
current_app = LocalProxy(get_current_app)

Estos proxies utilizan LocalStack y LocalProxy para aislar datos por hilo, garantizando que cada solicitud tenga su propio contexto sin compartir estado.

Gestión de Sesiones

Las sesiones se almacenan en memoria, con un ID único generado mediante UUID4. Se codifican en Base64 para incluirlos en cookies.

def open_session(self, app, request):
    sid = request.cookies.get(app.config['SESSION_COOKIE_NAME'])
    if sid:
        sid = base64.b64decode(sid).decode('utf-8')
    if sid and sid in self.sessions:
        return self.sessions[sid]
    sid = str(uuid.uuid4())
    session = Session(sid, permanent=app.config['SESSION_LIFETIME'])
    self.sessions[sid] = session
    return session

def save_session(self, response, app, session):
    sid = base64.b64encode(session.sid.encode('utf-8')).decode('utf-8')
    max_age = session.permanent or 31 * 86400
    response.set_cookie(
        app.config['SESSION_COOKIE_NAME'],
        sid,
        max_age=max_age,
        httponly=True
    )
    return response

El orden de operaciones es crítico: las sesiones deben abrirse antes de crear el contexto de solicitud.

Funciones de Ganchos

Se soportan funciones que se ejecutan antes de la solicitud y después de generar la respuesta.

def before_request(self, func):
    self.before_request_funcs.append(func)

def after_response(self, func):
    self.after_response_funcs.append(func)

Motor de Plantillas Jinja2

Se configura un entorno de plantillas para renderizar contenido dinámico.

def build_jinja_env(template_path='templates', root_path=None):
    path = root_path or os.getcwd()
    loader = FileSystemLoader(os.path.join(path, template_path))
    env = Environment(loader=loader, autoescape=guess_auto_escape, extensions=['jinja2.ext.autoescape'])
    env.globals.update({
        'url_for': url_for,
        'g': g,
        'session': session,
        'request': request
    })
    return env

Las plantillas pueden acceder directamente a variables globales como url_for, g, y request.

def render_template(template_name, **context):
    template = current_app.jinja_env.get_template(template_name)
    return make_response(template.render(context), mimetype='text/html')

Configuración

Una clase de configuración heredada de dict permite cargar valores por defecto y desde archivos externos, aprovechando métodos mágicos como __getitem__ y __setitem__.

Limitaciones y Futuras Mejoras

Este framework no incluye funcionalidades avanzadas como ORM, validación de entradas, pruebas automatizadas, logging o API RESTful. Sin embargo, representa un excelente punto de partida para aprender cómo funcionan los frameworks web internamente.

Ejemplo de Uso

Un pequeño ejemplo muestra autenticación básica con registro, login y uso de sesiones junto con plantillas HTML.

Etiquetas: Python werkzeug WSGI Flask Jinja2

Publicado el 9-5 19:07