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.