Hello World con Python, Django y PyCharm

Django es un Framework para el desarrollo de Aplicaciones Web en Python, que cada día goza de mayor popularidad, facilitando el desarrollo rápido de Aplicaciones Web de forma sencilla. En este Post explicamos como crear un Proyecto Django desde cero con el IDE de PyCharm, introducimos los principales conceptos de Django que veremos a través de un ejemplo, cómo ejecutar y depurar nuestro código, cómo Dockerizarlo, y cómo utilizar un servidor Web Gunicorn y un NGINX como Proxy Inverso con Docker Compose, además de algunos otros detalles.

Continuando con la serie de Posts sobre Python, en esta ocasión vamos a hablar un poco sobre Django, y a conocerlo a través de un ejemplo, disponible en el siguiente repo público de GitHub: GitHub – ElWillieES – django-hello-world

Django es un framework de desarrollo web de alto nivel, escrito en Python, que proporciona herramientas y funcionalidades que agilizan la creación de aplicaciones web robustas y seguras. Django sigue el patrón Modelo-Vista-Controlador (MVC) y destaca por su eficiencia y reutilización de código.

Django proporciona bastante documentación y ejemplos a través de su web (Django Project) y comparte su código fuente en GitHub (GitHub – Django).

Si estás comenzando con Python, quizás te interese leer primero el siguiente Post, antes de continuar: Hello World con Python y PyCharm.

Dicho esto, comenzamos con algunos conceptos básicos de Django.

Introducción a Django

Django: Projects & Apps

Django utiliza los conceptos de Proyectos y Aplicaciones (Projects & Apps), para de esta forma organizar el código. Un Projecto Django puede contener una o varias Aplicaciones. Es decir, sería la jerarquía: Proyecto Python -> Proyecto Django -> App Django

Dicho de otro modo, en un Proyecto Python, podemos crear uno o varios Proyectos Django, en cada uno de los cuales a su vez podemos crear una o varias Aplicaciones Django. En cualquier caso, la forma en la que podemos orgarnizar nuestro código entre diferentes Proyectos de Python, Proyectos Django, y Aplicaciones Django, es algo subjetivo sujeto a los gustos de cada uno. Lo importante, dividir la funcionalidad de una forma clara, y que cada App tenga un objetivo y funcionalidad claro, objetivo, y bien definido. A partir de ahí, para gustos hay colores.

Además, Django proporciona varias Aplicaciones por defecto (built-in Apps), que podemos utilizar OOB, y que nos pueden facilitar mucho la vida, como por ejemplo, el Panel de Administración de Django (App Admin), que proporciona una interfaz web para administrar y realizar operaciones CRUD (crear, leer, actualizar y eliminar) en los modelos de la aplicación, o la App Auth que proporciona autenticación junto con gestión de permisos, usuarios, grupos, etc..

Django: URLs, Views, Models, Templates

Django utiliza una aproximación del patrón Modelo-Vista-Controlador (MVC: Model-View-Controller) denominado Modelo-Vista-Plantilla (MVT: Model-View-Template). Por este motivo, una aplicación Django suele necesitar cuatro ficheros para poder construir y servir una página Web.

  • urls.py (URLs). Define dónde (qué URL) se muestra el contenido, y se trata de un fichero del Proyecto Django, aunque como veremos, es habitual crear ficheros urls.py por cada Aplicación Django, y luego importarlos todos en el urls.py del Proyecto.
  • views.py (Vistas o Views). Define qué contenido mostrar, y es un fichero de la Aplicación Django. Existen tres tipos de Vistas:
    • Vistas basadas en Funciones (FBVs: Function-based views). Son las más sencillas, y las únicas disponibles en las primeras versiones de Django. Las usamos en el ejemplo de este Post, se puede ver más abajo.
    • Vistas basadas en Clases (CBVs: Class-based views). Son más complejas, pero permite la reutilización y se puede extender.
    • Vistas basadas en Clases genéricas predefinidas (GCBVs: Generic Class Based Views). Proporcionadas OOB por Django, facilitan algunos casos de uso comunes (ej: forms, list views, paginación, etc).
  • models.py (Modelos o Models). Proporciona los datos de una base de datos, y es un fichero de la Aplicación Django.
  • Plantilla o Templates (ej: index.html). Proporciona el estilo, la presentación. Básicamente son son ficheros HTML, combinados con datos, CSS, JavaScript, imágenes (assets), etc., que pueden incluir cierta lógica (bastante básica). Tienen las ventaja de que pueden ser extendidas, por ejemplo, podemos crear una Plantilla base que defina la cabecera, menú y pié de nuestra Web, y que el resto de páginas hereden esta plantilla base, para implementar sobre ellas el contenido, de tal modo que futuros cambios de cabecera, menú o pié sería necesario realizarlos una única vez sobre la Plantilla base. Además, Django proporciona un lenguaje de etiquetas para el plantillado, que permite añadir contenido y cierta lógica, variables, etc. Tenemos dos opciones para el almacenamiento y gestión de las plantillas:
    • Almacenar las plantillas en cada Aplicación Django. Es la opción por defecto, en la que para cada Aplicación Django se necesita crear una carpeta templates, que contendrá otra carpeta con el nombre de la aplicación, que será donde se almacenen las Vistas. De esta forma quedarían distribuidas entre las diferentes aplicaciones.
    • Almacenar las plantillas en una ubicación única para todo el Proyecto Django. Requiere una pequeña configuración en el fichero settings.py.

El ciclo habitual sería el siguiente: Cuando Django recibe una petición HTTP, lo primero que hace es comprobar que se trata de una URL válida que tiene que ser gestionada por Django e identificar cuál es la Vista encarga de proporcionar su contenido (posiblemente a partir de una base de datos, es decir, el Modelo), para finalmente construir la respuesta HTTP a partir de una Plantilla (proporciona la presentación, el estilo, y quizás una lógica básica), que será el resultado que recibirá el cliente o usuario.

Django ORM

Django proporciona su propio ORM (Object-Relational Mapper), que soporta varias bases de datos, principalmente SQLite, PostgreSQL, MySQL, y Oracle, fácil de usar y muy productivo. Por defecto, y por simplificar el desarrollo, se utiliza SQLite.

Un ORM proporciona una capa de abstracción que permite interactuar con la base de datos utilizando objetos y consultas en lugar de escribir consultas SQL, independientemente del motor de base de datos utilizado. Permite definir modelos de datos como clases de Python, donde cada atributo de la clase representa un campo de la tabla de la base de datos, y sobre esta definición poder escribir consultas, hacer filtros y/o agregaciones, gestionar las migraciones de esquemas de base de datos (es decir, los cambios de esquema como creación y modificación de tablas), etc.

Con un ORM, podemos escribir el mismo código fuente independientemente del motor de base de datos utilizado. Lo único diferente que tendremos que hacer, es configurar en el fichero settings.py del Proyecto Django la base de datos que deseamos utilizar (sección DATABASES).

Vamos a verlo junto con otros detalles, pasa a paso, a través de un ejemplo utilizando Django 3.0 (en el siguiente Post utilizaremos Django 4.0).

Creación de un nuevo Proyecto Python con un Proyecto Django en PyCharm

Abrimos PyCharm y creamos un nuevo proyecto de Python en blanco, sin incluir el welcome script (main.py).

De momento, sólo tenemos un Proyecto Python.

Añadimos un fichero requirements.txt a la raiz del proyecto, e incluimos las librerías o paquetes que deseamos utilizar con la versión que queremos, en nuestro caso, django==3.0. Click en Install requirement para que PyCharm instale la librería en el Virtual Environment del proyecto, y que la podamos utilizar.

En la ventana de Terminal de PyCharm, ejecutamos los siguientes dos comandos, el primero para comprobar la versión instalada de Django, y el segundo para crear un Proyecto Django, es decir, crear los ficheros que necesita Django para funcionar (manage.py y la carpeta del Proyecto Django, en nuestro caso, la carpeta hello_world).

python -m django --version
django-admin startproject hello_world .

Ahora ya tenemos un Proyecto Python que incluye en su interior un Proyecto Django. Vamos a revisar brevemente los ficheros creados por Django:

  • settings.py. Contiene la configuración del Proyecto Django.
  • urls.py. Se encarga del enrutamiento, es decir, define qué URLs (ej: páginas) serán servidas por Django y las asocia a las Vistas correspondientes (vistas o clases de una Aplicación Django), para lo cual pueden utilizarse expresiones regulares (RegEx). Cuando un Proyecto Django tiene varias Aplicaciones, es posible crear manualmente un fichero urls.py en cada Aplicación, y referenciarlos desde el fichero urls.py del Proyecto Django utilizando la función include. Esto facilita una mejor organización de las URLs del proyecto. Lo vemos más adelante, con un ejemplo.
  • wsgi.py (Web Server Gateway Interface) y asgi.py (Asynchronous Server Gateway Interface). Ficheros de configuración de WSGI y ASGI.
  • mange.py. No es parte del Proyecto Django en sí, pero permite ejecutar varios comandos Django, como por ejemplo, arrancar el servidor Web de Django, crear una Aplicación Django, ejecutar las migraciones de base de datos, ejecutar las Pruebas Unitarias de Django, etc.

Sólo con esto, ya podríamos arrancar el servidor web local de Django (útil sólo para propósitos de desarrollo), aunque ahora es aún un proyecto vacío. Para hacerlo, debemos ejecutar el siguiente comando en la ventana de Terminal de PyCharm.

python manage.py runserver

Veremos algún Warning, por ejemplo, invitándonos a ejecutar las migraciones de base de datos de las aplicaciones built-in que incluye Django por defecto. Este proceso, básicamente consiste en inicializar una base de datos para el correcto funcionamiento de la aplicación o aplicaciones, algo que de momento no es estrictamente necesario.

Si abrimos un navegador, podremos acceder al servidor Web de Django que acabamos de levantar, de momento, con el contenido por defecto que proporciona Django. Podemos cerrar el navegador y pulsar Ctrl-C en la ventana de Terminal de PyCharm, para detenerlo.

Para ejecutar las migraciones de base de datos, es suficiente con ejecutar el siguiente comando en la ventana Terminal de PyCharm

python manage.py migrate

Crear una Aplicación Django y utilizarla en nuestro Proyecto Django

Para continuar, vamos a crear una Aplicación Django. Para ello ejecutaremos el siguiente comando desde una ventana de Terminal de PyCharm.

python manage.py startapp hello_world_page

Podremos observar que se ha creado una nueva carpeta para nuestra App, que de forma resumida, contiene:

  • apps.py. Permite configurar y personalizar la propia aplicación Django, como por ejemplo: configurar el nombre y etiqueta de la aplicación, personalizar las configuraciones que necesita la aplicación (ej: connection strings, URLs, internalización y localización, etc.), definir tareas de inicialización para que se ejecuten cuando se carge la App, etc.
  • admin.py. Permite configurar y personalizar la forma en que los modelos de una aplicación Django se administran en el Panel de Administración de Django. En este fichero de configuración se puede registrar modelos, personalizar la apariencia y campos mostrados, definir acciones, configurar filtros y búsquedas, etc.
  • models.py. Permite definir los modelos de datos de la aplicación, que Django es capaz de tranformar automáticamente a una base de datos (ej: tablas). Cada clase de modelo se mapea a una tabla en la base de datos y define los campos y relaciones que se utilizarán para almacenar y consultar los datos. Los modelos se pueden utilizar en otras partes de la aplicación, como en las vistas y plantillas, para interactuar con los datos almacenados en la base de datos. Django proporciona un ORM (Object-Relational Mapping) que permite realizar operaciones de creación, lectura, actualización y eliminación de registros en la base de datos utilizando los modelos definidos.
  • tests.py. Permite definir las pruebas unitarias de la aplicación Django: definir clases y métodos de pruebas, y configurar el entorno de prueba, para facilitar la ejecución de las pruebas unitarias.
  • views.py. Permite definir las vistas, que son las funciones o clases responsables de gestionar las peticiones y respuestas HTTP (controlan la lógica y presentación de una página o recurso web).
  • migrations/. Almacenar los archivos de migración de base de datos, que permiten realizar cambios de estructura en la base de datos de manera controlada y versionada, manteniendo la integridad de los datos existentes. Puede contener múltiples archivos de migración, cada uno de los cuales representa un paso específico en la evolución de la base de datos.

Aunque hemos creado una Aplicación Django, el Proyecto Django aún no sabe de su existencia. Son dos elementos independientes, que tenemos que relacionar. Para ellos, editaremos el fichero settings.py del Proyecto Django, y añadiremos la siguiente línea al final de la opción INSTALLED_APPS. El valor que añadimos está relacionado con el contenido del fichero apps.py de la Aplicación Django que deseamos ejecutar dentro de nuestro Proyecto Django.

Ahora vamos a escribir la Vista de nuestra Aplicación Django, dentro del fichero views.py, que consistirá en añadir las siguientes tres líneas de código. Como comentábamos al principio, vamos a utilizar un Vista basada en una Función (FBV: Function-based view). En nuestro caso, vamos a definir una función que devuelve directamente la respuesta HTTP, sin utilizar Plantillas ni Modelos, que para este primer ejemplo de Hello World, es más que suficiente, aunque en la práctica no sea algo muy habitual.

Ahora que ya tenemos definida nuestra Vista, llega el momento de definir las URLs que deseamos asociar a dicha Vista.

En lugar de incluir esta asociación en el fichero urls.py del Proyecto Django, vamos a crear un fichero urls.py en la Aplicación Django para configurar ahí el enrutamiento de dicha Aplicación, y lo vamos a referenciar (include) en el fichero urls.py del Proyecto.

Para hacer esto, lo primero es crear el fichero urls.py de la Aplicación Django y configurar el enrutamiento que deseamos, tal y como se muestra en el siguiente ejemplo: una petición HTTP sobre la raíz, se resolverá llamando a la Vista helloWorldPageView que acabamos de crear. Por este motivo, necesitamos importar el fichero views del directorio actual (la línea que comienza por from .views). Un detalle importante, es que a las URLs que definimos, les podemos asignar un nombre (ej: helloWorld, home, etc), algo que como veremos resulta útil en diversas ocasiones y además es bastante habitual.

Por otro lado, en el fichero urls.py del Proyecto Django configuraremos también el enrutamiento, en este caso diremos que cualquier petición a hello-world/ se resuelva conforme se defina en el fichero urls.py de nuestra Aplicación Django, que acabamo de configurar en el punto anterior. Tan sólo hay que escribir dos líneas:

Ya sólo queda volver a arrancar el servidor Web local de Django, para probar que todo funciona correctamente. Ejecutaremos del siguiente comando desde la ventana de Terminal de PyCharm.

python manage.py runserver

Y podremos ver el resultado utilizando un navegador.

Ejecutar y Depurar en PyCharm

Ya tenemos nuestra primera aplicación Django, pero nos queda ver cómo podemos ejecutar y dupurar nuestro código.

Si bien es cierto que podemos ejecutarlo lanzando el comando runserver de Django desde la ventana de Terminal de PyCharm, esto ni resulta cómodo, ni nos permite poder depurar la ejecución de nuestro código. Para solucionarlo, en PyCharm, desde la opción de menú «Run -> Edit Configurations«, podemos añadir una configuración de ejecución, que arranque el servidor Web de Django, tal y como se muestra en la siguiente pantalla.

Hecho esto, podremos tanto ejecutar (Run) como ejecutar en modo depuración (Debug) nuestra aplicación con un sólo click, en este último caso, pudiendo además poner puntos de interrupción, ejecutar paso a paso, evaluar variables, etc.

Productivizar nuestro Proyecto Django con Gunicorn, Docker, y NGINX

Hasta este momento, hemos estado focalizado en el entorno de desarrollo, en crear nuestro primer Proyecto Django y ejecutarlo en local, utilizando PyCharm y el servidor Web de Django. Ahora vamos a ver cómo podríamos llevarlo a Producción, es decir, que se ejecute con servidor Web más profesional como Gunicorn, de forma dockerizada, incluso añadiendo un Proxy Inverso como NGINX (también dockerizado).

Gunicorn (Green Unicorn) es un servidor de aplicaciones para UNIX. No funciona bien en Windows, por lo que en mi portátil seguiré utilizando el servidor Web de Django para ejecutar y depurar desde PyCharm, pero utilizaré Gunicorn para empaquetarlo en Docker, que será la forma en la que se ejecutará en producción, y que también puedo probar en mi propio portátil.

Editaremos el fichero requirements.txt y añadiremos una línea para incluir el paquete de gunicorn, en la versión que deseemos, de forma similar a como se muestra a continuación. Haremos también click en la opción «Install requirement» de PyCharm para instalarlo en el Virtual Environment.

Como además de los propios ficheros de nuestro Proyecto Python, también necesitamos tener otros ficheros como Dockerfiles, vamos a reorganizar mejor los ficheros y carpetas de nuestro proyecto Python. Básicamente creamos un carpeta app en la que movemos los ficheros de nuestro Proyecto Python, todo excepto requirements.txt, que lo mantenemos en la raíz. A continuación se muestra como quedaría.

Ahora que ya tenemos Gunicorn y hemos reorganizado nuestro proyecto, vamos a dockerizarlo. Para ello añadiremos un nuevo fichero Dockerfile en la raíz de nuestro proyecto Python, en el que añadiremos el siguiente contenido. Básicamente, partiendo de una imagen oficial de Python, copiamos los ficheros de nuestra aplicación, instalamos las librerías que necesitamos (requirements.txt) y ejecutamos gunicorn.

FROM python:3.9.15-slim-bullseye

ENV TZ="Europe/Madrid"

RUN mkdir -p /usr/src/app
COPY app /usr/src/app/
COPY requirements.txt /usr/src/app/

WORKDIR /usr/src/app

RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt

CMD cd /usr/src/app && gunicorn hello_world.wsgi:application --bind 0.0.0.0:8000

Para probarlo, desde una ventana de Terminal podemos construir la imagen Docker (docker build) y arrancar un contenedor con nuestra imagen (docker run). Hecho eso, podemos comprobar que el contenedor ha arrancado correctamente (docker ps) y probar el acceso a nuestra aplicación con un navegador. Cuando acabemos, podemos parar el contenedor (docker stop).

docker build -t django-hello-world .
docker run -it --rm -d -p 8000:8000 --name django-hello-world django-hello-world
docker stop django-hello-world

Ahora tenemos que añadir un Proxy Inverso (NGINX), pero antes de eso, vamos a crear un fichero docker-compose-yml, para poder arrancar nuestra aplicación Django con Docker Compose, luego añadiremos el NGINX. Para ello, creamos el fichero docker-compose.yml con el siguiente contenido.

version: '3.4'

services:
  django-hello-world:
    image: django-hello-world
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "8000:8000"

Para probarlo, aprovechando que justo acabamos antes de crear la imagen Docker, desde una ventana de Terminal podemos arrancar un contenedor con nuestra imagen (docker-compose up -d). Hecho eso, podemos comprobar que el contenedor ha arrancado correctamente (docker ps) y probar el acceso a nuestra aplicación con un navegador. Cuando acabemos, podemos parar y eliminar el contenedor (docker-compose down).

docker-compose up -d
docker ps
docker-compose down

Lo siguiente es crear el fichero de configuración del NGINX (nginx.conf) para que actúe como Proxy Inverso escuchando en el puerto tcp-80, y añadir un NGINX al fichero docker-compose.yml utilizando dicho fichero de configuración. Empezaremos creando el fichero nginx.conf.

events {
}

http {

   server {
      listen        80;

      client_max_body_size 1G;

      location / {
         proxy_pass http://django-hello-world:8000;

         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
      }
   }

}

Lo siguiente es actualizar el fichero docker-compose.yml para añadir un contenedor para el NGINX.

version: '3.4'

services:
  django-hello-world:
    image: django-hello-world
    container_name: django-hello-world
    restart: unless-stopped
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "8000:8000"
  nginx:
    image: nginx:1.23.4
    container_name: nginx
    restart: unless-stopped
    environment:
      TZ: ${TZ}
    ports:
      - 80:80
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf

Con esto, ya estaría. Para probarlo, de nuevo desde una ventana de Terminal podemos arrancar los contenedores con nuestra imagen y con el NGINX (docker-compose up -d). Hecho eso, podemos comprobar que ambos contenedores han arrancados correctamente (docker ps) y probar el acceso a nuestra aplicación con un navegador, tanto directamente a Gunicorn (puerto tcp-8000) como a través del NGINX (puerto tcp-80). Cuando acabemos, podemos parar y eliminar los contenedores (docker-compose down).

docker-compose up -d
docker ps
docker-compose down

Con esto, ya hemos prácticamente acabado nuestro Hello World en Django, desde crear nuestro proyecto Python en PyCharm desde cero, hasta tenerlo dockerizado con un NGINX como Proxy Inverso, y pudiendo ejecutarlo y depurar en local. Ya sólo quedaría añadirlo a Git, pasarlo por Sonarqube (crear un fichero sonar-project.properties), crear un README.md, etc.

Despedida y Cierre

Con esto, ya hemos prácticamente acabado nuestro Hello World en Django, desde crear nuestro proyecto Python en PyCharm desde cero, hasta tenerlo dockerizado con un NGINX como Proxy Inverso, pudiendo ejecutarlo y depurar en local. Ya sólo quedaría añadirlo a Git, pasarlo por Sonarqube (crear un fichero sonar-project.properties), crear un README.md, etc.

Además, tienes disponible este ejemplo en el siguiente repo público de GitHub: GitHub – ElWillieES – django-hello-world

Poco más por hoy. Como siempre, confío que la lectura resulte de interés.