From 71d0a3c4d8b754743c5d795ec3cad0208f3c07d5 Mon Sep 17 00:00:00 2001 From: Manuel Vergara Date: Sun, 16 Jun 2024 18:00:16 +0200 Subject: [PATCH] Add new exercise --- catch-all/05_redis_flask_docker/.dockerignore | 90 +++++++++ catch-all/05_redis_flask_docker/Dockerfile | 12 ++ catch-all/05_redis_flask_docker/README.md | 188 ++++++++++++++++++ catch-all/05_redis_flask_docker/app.py | 21 ++ catch-all/05_redis_flask_docker/config.py | 11 + .../05_redis_flask_docker/docker-compose.yaml | 19 ++ .../05_redis_flask_docker/requirements.txt | 16 ++ catch-all/README.md | 1 + 8 files changed, 358 insertions(+) create mode 100644 catch-all/05_redis_flask_docker/.dockerignore create mode 100644 catch-all/05_redis_flask_docker/Dockerfile create mode 100644 catch-all/05_redis_flask_docker/README.md create mode 100644 catch-all/05_redis_flask_docker/app.py create mode 100644 catch-all/05_redis_flask_docker/config.py create mode 100644 catch-all/05_redis_flask_docker/docker-compose.yaml create mode 100644 catch-all/05_redis_flask_docker/requirements.txt diff --git a/catch-all/05_redis_flask_docker/.dockerignore b/catch-all/05_redis_flask_docker/.dockerignore new file mode 100644 index 0000000..da2b6eb --- /dev/null +++ b/catch-all/05_redis_flask_docker/.dockerignore @@ -0,0 +1,90 @@ +README.md + +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +# .env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ diff --git a/catch-all/05_redis_flask_docker/Dockerfile b/catch-all/05_redis_flask_docker/Dockerfile new file mode 100644 index 0000000..855e16b --- /dev/null +++ b/catch-all/05_redis_flask_docker/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-alpine AS builder + +WORKDIR /app + +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 5000 + +CMD ["python", "app.py"] + diff --git a/catch-all/05_redis_flask_docker/README.md b/catch-all/05_redis_flask_docker/README.md new file mode 100644 index 0000000..6bc5dcf --- /dev/null +++ b/catch-all/05_redis_flask_docker/README.md @@ -0,0 +1,188 @@ +# Crear una API Caching con Redis, Flask y Docker + + + +![](https://res.cloudinary.com/practicaldev/image/fetch/s--MflWnlWv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4350/1%2AgEpkD_3NMTxK-w96c_QBBA.png) + +## Prueba 1: Sin Redis + +Primero vamos a hacer una prueba de la aplicación sin redis. + +Vamos al directorio donde queremos trabajar, creamos un entorno virtual y lo activamos: + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +Ahora instalamos las dependencias en nuestro entorno: + +```bash +(venv) pip install Flask redis flask_caching requests +``` + +Y guardamos estas dependencias en un archivo `requirements.txt`: + +```bash +(venv) pip freeze > requirements.txt +``` +Vamos a crear un archivo `app.py` con el siguiente contenido: + +```python +import requests +from flask import Flask, request, jsonify + +app = Flask(__name__) + + +@app.route("/universities") +def get_universities(): + API_URL = "http://universities.hipolabs.com/search?country=" + search = request.args.get('country') + r = requests.get(f"{API_URL}{search}") + return jsonify(r.json()) + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5000) +``` + +Ahora vamos a ejecutar la aplicación: + +```bash +export FLASK_APP=app.py +export FLASK_ENV=development +flask run +``` + +Y si nos vamos a postman podremos comprobar cuanto tarda en responder la petición: + +![](https://res.cloudinary.com/practicaldev/image/fetch/s--cFeIXNRd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2898/1%2A9o8_94EDMwGO-BWWKnshBA.png) + + +## Prueba 2: Dockerizar nuestra aplicación + +Vamos a dockerizar nuestra aplicación, para ello vamos a crear un archivo `Dockerfile` con el siguiente contenido: + +```Dockerfile +FROM python:3.12-alpine AS builder + +WORKDIR /app + +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 5000 + +CMD ["python", "app.py"] +``` + +Vamos a crear también un archivo `docker-compose.yaml` con el siguiente contenido: + +```yaml +version: '3.8' + +services: + api: + container_name: app-python-flask-with-redis + build: . + env_file: + - .env + ports: + - '5000:5000' + depends_on: + - redis + + redis: + image: redis:7.0-alpine + container_name: redis-python + ports: + - '6379:6379' +``` + +Incluimos también el contenedor de Redis. Lanzamos nuestra aplicación con el comando: + +```bash +docker-compose up -d --build +``` + +Si vemos `docker ps` veremos que tenemos dos contenedores corriendo. También podemos revisar los logs del contenedor de la aplicación con `docker logs app-python-flask-with-redis`. + +Comprobamos que nuestra aplicación sigue funcionando en docker igual que lo hacía en local. + +## Prueba 3: Añadir Redis a nuestra aplicación + +Ahora vamos a añadir Redis a nuestra aplicación. Vamos a modificar el archivo `app.py` para que use Redis: + +```python +import requests +from flask import Flask, jsonify, request +from flask_caching import Cache + +app = Flask(__name__) +app.config.from_object('config.BaseConfig') +cache = Cache(app) + +@app.route("/universities") +@cache.cached(timeout=30, query_string=True) +def get_universities(): + API_URL = "http://universities.hipolabs.com/search?country=" + search = request.args.get('country') + r = requests.get(f"{API_URL}{search}") + return jsonify(r.json()) + +if __name__ == '__main__': + app.run(host='0.0.0.0') +``` + + +Y vamos a crear un archivo `config.py` con el siguiente contenido: + +```python +import os + +class BaseConfig(object): + CACHE_TYPE = os.environ['CACHE_TYPE'] + CACHE_REDIS_HOST = os.environ['CACHE_REDIS_HOST'] + CACHE_REDIS_PORT = os.environ['CACHE_REDIS_PORT'] + CACHE_REDIS_DB = os.environ['CACHE_REDIS_DB'] + CACHE_REDIS_URL = os.environ['CACHE_REDIS_URL'] + CACHE_DEFAULT_TIMEOUT = os.environ['CACHE_DEFAULT_TIMEOUT'] +``` + +Este fichero recoge las variables de entorno que vamos a usar en nuestra aplicación. Vamos a crear un archivo `.env` con el siguiente contenido: + +```bash +# .e +CACHE_TYPE=redis +CACHE_REDIS_HOST=redis +CACHE_REDIS_PORT=6379 +CACHE_REDIS_DB=0 +CACHE_REDIS_URL=redis://redis:6379/0 +CACHE_DEFAULT_TIMEOUT=300 +``` + +Al finalizar la práctica, tendremos esta estructura: + +``` +. +├── app.py +├── config.py +├── docker-compose.yaml +├── Dockerfile +├── .env +└── requirements.txt +``` + +Volvemos a lanzar nuestra aplicación con `docker-compose up -d --build` y comprobamos que todo sigue funcionando correctamente. + +Volvemos a lanzar la misma petición desde postman y comprobamos que la respuesta es mucho más rápida que antes: + +![](https://res.cloudinary.com/practicaldev/image/fetch/s--mHDsWyzq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2922/1%2Au4a8OUBQu2gBzvc6iL5nsw.png) + + +Podemos probar con otros países, la primera vez tardará más porque no estará en caché, pero las siguientes veces será mucho más rápido. + +Esta es la magia de Redis, una base de datos en memoria que nos permite almacenar datos en caché y acelerar nuestras aplicaciones 🚀 + diff --git a/catch-all/05_redis_flask_docker/app.py b/catch-all/05_redis_flask_docker/app.py new file mode 100644 index 0000000..a65eb70 --- /dev/null +++ b/catch-all/05_redis_flask_docker/app.py @@ -0,0 +1,21 @@ +import requests +from flask import Flask, jsonify, request +from flask_caching import Cache + + +app = Flask(__name__) +app.config.from_object('config.BaseConfig') +cache = Cache(app) + + +@app.route("/universities") +@cache.cached(timeout=30, query_string=True) +def get_universities(): + API_URL = "http://universities.hipolabs.com/search?country=" + search = request.args.get('country') + r = requests.get(f"{API_URL}{search}") + return jsonify(r.json()) + + +if __name__ == '__main__': + app.run(host='0.0.0.0') diff --git a/catch-all/05_redis_flask_docker/config.py b/catch-all/05_redis_flask_docker/config.py new file mode 100644 index 0000000..b6e3922 --- /dev/null +++ b/catch-all/05_redis_flask_docker/config.py @@ -0,0 +1,11 @@ +import os + + +class BaseConfig(object): + CACHE_TYPE = os.environ['CACHE_TYPE'] + CACHE_REDIS_HOST = os.environ['CACHE_REDIS_HOST'] + CACHE_REDIS_PORT = os.environ['CACHE_REDIS_PORT'] + CACHE_REDIS_DB = os.environ['CACHE_REDIS_DB'] + CACHE_REDIS_URL = os.environ['CACHE_REDIS_URL'] + CACHE_DEFAULT_TIMEOUT = os.environ['CACHE_DEFAULT_TIMEOUT'] + diff --git a/catch-all/05_redis_flask_docker/docker-compose.yaml b/catch-all/05_redis_flask_docker/docker-compose.yaml new file mode 100644 index 0000000..2c283c5 --- /dev/null +++ b/catch-all/05_redis_flask_docker/docker-compose.yaml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + api: + container_name: app-python-flask-with-redis + build: . + env_file: + - .env + ports: + - '5000:5000' + depends_on: + - redis + + redis: + image: redis:7.0-alpine + container_name: redis-python + ports: + - '6379:6379' + diff --git a/catch-all/05_redis_flask_docker/requirements.txt b/catch-all/05_redis_flask_docker/requirements.txt new file mode 100644 index 0000000..a5d9e04 --- /dev/null +++ b/catch-all/05_redis_flask_docker/requirements.txt @@ -0,0 +1,16 @@ +async-timeout==4.0.3 +blinker==1.8.2 +cachelib==0.9.0 +certifi==2024.6.2 +charset-normalizer==3.3.2 +click==8.1.7 +Flask==3.0.3 +Flask-Caching==2.3.0 +idna==3.7 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +redis==5.0.5 +requests==2.32.3 +urllib3==2.2.1 +Werkzeug==3.0.3 diff --git a/catch-all/README.md b/catch-all/README.md index 8b831d9..b39cf44 100644 --- a/catch-all/README.md +++ b/catch-all/README.md @@ -12,3 +12,4 @@ Aquí iré dejando scripts y ejercicios que se me ocurran, con lo que no hay un | [Descifrador wargame](./02_scripts_descifrador_wargame/) | Script descifrador de código al estilo wargame | intermedio | | [Clima España](./03_clima/) | Script conectado a API AEMET | intermedio | | [acortador de enlaces](./04_acortador_url/) | Script para acortar enlaces y redirigirlos con app Flask | intermedio | +| [API Caching](./05_redis_flask_docker) | App Flask con caché en Redis y con una bbdd | intermedio |