You've already forked Curso-lenguaje-python
Compare commits
131 Commits
af9e0b66d9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 92e473fdfe | |||
| 6e97f2bea1 | |||
| fa1ee4dd64 | |||
| b9884d6560 | |||
| 9245dbb9af | |||
| 2c4f14a886 | |||
| 1fa360a109 | |||
| a2badd511d | |||
| 68b9bf32a3 | |||
| 21c7e1e6c6 | |||
| dc9e81f06e | |||
| dd48618093 | |||
| 3e47d5a7ee | |||
| d59e31205b | |||
| e2767e99af | |||
| 65468e4115 | |||
| 4f2264748e | |||
| a73e8a0222 | |||
| 84a2519f6c | |||
| 88a26dae34 | |||
| b672828792 | |||
| 40fad6bae8 | |||
| 17a7e04180 | |||
| 6cb79e3418 | |||
| 28f9bb389d | |||
| f525beaf11 | |||
| 4756d756ac | |||
| 89959d29ee | |||
| 898da84dc3 | |||
| bba89794a2 | |||
| 78552c227a | |||
| 9a60b44822 | |||
| e856e99ac3 | |||
| 7102b86e6e | |||
| 50513ff393 | |||
| 0daba91bbb | |||
| a7cefe06d0 | |||
| ecd77967a0 | |||
| be39d5b1d3 | |||
| d7640c2a52 | |||
| 2b946f3327 | |||
| 5ff0fa4f2a | |||
| a421d3b292 | |||
| 84e3344d49 | |||
| 56ac284176 | |||
| 94e6ca1027 | |||
| 819aaaa1f5 | |||
| d8ca020c98 | |||
| fbecbbf0a1 | |||
| 5821636a01 | |||
| 62d0ec78b5 | |||
| 71d0a3c4d8 | |||
| d7c1cee2a8 | |||
| c1c5883a56 | |||
| 485d16e930 | |||
| 35727e1664 | |||
| a55a297cdb | |||
| 69389a8940 | |||
| 3d5486414f | |||
| d7252c6782 | |||
| d4c5d8a50f | |||
| 5d7e3302c5 | |||
| 0d95645a93 | |||
| 3704f6c61e | |||
| d505f6ee23 | |||
| bcb3977c07 | |||
| 6ae9c26e65 | |||
| d1a05d481b | |||
| 21313f990b | |||
| 213a859a7d | |||
| 352889e2ba | |||
| 2de0131f95 | |||
| f7043ed2ae | |||
| d828243927 | |||
| 04be4f610d | |||
| ace46c757a | |||
| 4abbfdf261 | |||
| 92ed9c3beb | |||
| c86c4f8b21 | |||
| 4847e32f04 | |||
| 371cd18bf0 | |||
| cb7b2c2e13 | |||
| 4890661532 | |||
| 222649cd8d | |||
| 8f2913d463 | |||
| 7f601e84a7 | |||
| 7dddcfaa28 | |||
| 386c544294 | |||
| 9801b42111 | |||
| 5be31fd800 | |||
| 80e7e4a21c | |||
| b8187724d2 | |||
| c0b6b881e4 | |||
| bd197dc519 | |||
| 81e279e30a | |||
| 23805c8241 | |||
| cf745e8c8d | |||
| 700c489428 | |||
| 658f94c62b | |||
| d15f5b4eb6 | |||
| 062908b4ee | |||
| 72ccded05d | |||
| 17d758d75b | |||
| 9ad2161b99 | |||
| 36973a9662 | |||
| 8109c2fe30 | |||
| f09fa0cbf5 | |||
| ec29b5cde0 | |||
| 653e96cc2b | |||
| 3ce3338795 | |||
| 818ec91771 | |||
| bb39d2366f | |||
| 1f8a74e791 | |||
| 661b027ccc | |||
| 7c2ccb390c | |||
| d49859c627 | |||
| 159d9490a4 | |||
| 4c142e7432 | |||
| e300dee83f | |||
| 490afce41f | |||
| ab95e6f805 | |||
| 8896ac39c2 | |||
| 3402445513 | |||
| 54f7024e20 | |||
| 51ed9bc844 | |||
| 9d7531b0f1 | |||
| be1177090b | |||
| a307064326 | |||
| 85549ac613 | |||
| 086b1ca614 | |||
| 5b62da6cf0 |
33
.gitignore
vendored
33
.gitignore
vendored
@@ -127,9 +127,11 @@ celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
*.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
myenv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
@@ -164,3 +166,34 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
||||
# certificate files
|
||||
|
||||
# Ignore all certificates in this directory
|
||||
*.pem
|
||||
*.key
|
||||
*.csr
|
||||
|
||||
|
||||
# Ignore binary files of mitmproxy
|
||||
|
||||
mitmproxy/
|
||||
|
||||
|
||||
# Logs
|
||||
bot.log*
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Ignore database files
|
||||
rss.db
|
||||
|
||||
# Ignore vscode settings
|
||||
.vscode
|
||||
|
||||
# Ignore volume files
|
||||
rabbitmq/
|
||||
rabbitmq/*
|
||||
*_data
|
||||
*_data/*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
blinker==1.6.3
|
||||
blinker==1.7.0
|
||||
click==8.1.7
|
||||
Flask==3.0.0
|
||||
Flask==3.0.2
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
Jinja2==3.1.3
|
||||
MarkupSafe==2.1.5
|
||||
Werkzeug==3.0.1
|
||||
36
README.md
36
README.md
@@ -10,6 +10,7 @@ Este repositorio contiene los apuntes tomados en diversos cursos de python refle
|
||||
| [30 days of python](./30-days-of-python/README.md) | Intermedio | --- | 25 horas | [Repo Asabeneh](https://github.com/Asabeneh/30-Days-Of-Python) |
|
||||
| [Cajón de sastre](./catch-all/README.md) | Intermedio | --- | continua | [Personal](https://vergaracarmona.es) |
|
||||
| [scripts-hacking-etico](./scripts-hacking-etico/README.md) | Avanzado | --- | 5 horas | [ChapGPT](https://www.chat.openai.com), [Canal Telegram](https://t.me/seguridadinformatic4) |
|
||||
| [Python ofensivo](./python-ofensivo/README.md) | Avanzado | 35 horas | 100 horas | [hack4u](https://hack4u.io) |
|
||||
|
||||
Las prácticas y ejercicios aquí contenidos son los que hice mediante los cursos mencionados o por investigación propia, con mucho café para combatir el insomnio. Realmente, los apuntes no fueron pensados para compartirlos, por ello pueden tener lagunas de información o contenido adicional respecto al curso, ya que se redactaron para recordar procedimientos y conceptos que EMHO me parecieron relevantes. Teniendo estos documentos tan completos y entendiendo que el conocimiento debe ser libre, se decidió compartirlos. Si encuentras **cualquier error puedes abrir una issue o contactar conmigo**.
|
||||
|
||||
@@ -24,10 +25,9 @@ Recuerda,
|
||||
<br>
|
||||
|
||||
## Información sobre python
|
||||
|
||||
<details>
|
||||
<summary><strong> Qué es python según chatGPT 🤖</strong></summary>
|
||||
|
||||
|
||||
Python es un lenguaje de programación interpretado y de alto nivel. Python se destaca por su sintaxis clara y legible, lo que lo hace muy accesible tanto para principiantes como para programadores experimentados.
|
||||
|
||||
Una de las características distintivas de Python es su enfoque en la legibilidad del código, lo que se conoce como el principio "bello es mejor que feo" (beautiful is better than ugly). Esto se logra mediante el uso de una sintaxis clara y estructurada que facilita la comprensión y el mantenimiento del código.
|
||||
@@ -43,7 +43,7 @@ En resumen, Python es un lenguaje de programación de alto nivel, interpretado y
|
||||
|
||||
<details>
|
||||
<summary><strong>Historia de python 🏛️</strong></summary>
|
||||
Python fue creado a finales de los años ochenta por [Guido van Rossum](https://es.wikipedia.org/wiki/Guido_van_Rossum) en Stichting Mathematisch Centrum (CWI), en los Países Bajos, como un sucesor del lenguaje de programación ABC, capaz de manejar excepciones e interactuar con el sistema operativo Amoeba.
|
||||
Python fue creado a finales de los años ochenta por [Guido van Rossum](https://es.wikipedia.org/wiki/Guido_van_Rossum) en Stichting Mathematisch Centrum (CWI), en los Países Bajos, como un sucesor del lenguaje de programación ABC, capaz de manejar excepciones e interactuar con el sistema operativo Amoeba.
|
||||
|
||||
El nombre del lenguaje proviene de la afición de su creador por los humoristas británicos [Monty Python](https://youtu.be/aQqhR26FOW8).
|
||||
|
||||
@@ -54,12 +54,38 @@ Guido van Rossum es el principal autor de Python, y su continuo rol central en d
|
||||
> Guido van Rossum
|
||||
|
||||
En 2019, Python fue el lenguaje de programación más popular en GitHub, superando a Java, el segundo lenguaje más popular, por más de 1 millón de repositorios.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>PEP 20 - Zen de Python 📄</strong></summary>
|
||||
El Zen de Python es una colección de 20 principios de software que influyen en el diseño del Lenguaje de Programación Python, de los cuales 19 fueron escritos por Tim Peter en junio de 1999. El texto es distribuido como dominio público:
|
||||
|
||||
```
|
||||
Bello es mejor que feo.
|
||||
Explícito es mejor que implícito.
|
||||
Simple es mejor que complejo.
|
||||
Complejo es mejor que complicado.
|
||||
Plano es mejor que anidado.
|
||||
Espaciado es mejor que denso.
|
||||
La legibilidad es importante.
|
||||
Los casos especiales no son lo suficientemente especiales como para romper las reglas.
|
||||
Sin embargo la practicidad le gana a la pureza.
|
||||
Los errores nunca deberían pasar silenciosamente.
|
||||
A menos que se silencien explícitamente.
|
||||
Frente a la ambigüedad, evitar la tentación de adivinar.
|
||||
Debería haber una, y preferiblemente solo una, manera obvia de hacerlo.
|
||||
A pesar de que eso no sea obvio al principio a menos que seas Holandés.
|
||||
Ahora es mejor que nunca.
|
||||
A pesar de que nunca es muchas veces mejor que *ahora* mismo.
|
||||
Si la implementación es difícil de explicar, es una mala idea.
|
||||
Si la implementación es fácil de explicar, puede que sea una buena idea.
|
||||
Los espacios de nombres son una gran idea, ¡tengamos más de esos!
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Últimas versiones 🔄</strong></summary>
|
||||
Python 2.7.x (última versión de la serie Python 2.x) fue oficialmente descontinuado el 1 de enero de 2020 (paso inicialmente planeado para 2015), por lo que ya no se publicarán parches de seguridad y otras mejoras para él. Con el final del ciclo de vida de Python 2, solo tienen soporte la rama Python 3.6.x y posteriores.
|
||||
Python 2.7.x (última versión de la serie Python 2.x) fue oficialmente descontinuado el 1 de enero de 2020 (paso inicialmente planeado para 2015), por lo que ya no se publicarán parches de seguridad y otras mejoras para él. Con el final del ciclo de vida de Python 2, solo tienen soporte la rama Python 3.6.x y posteriores.
|
||||
|
||||
Con Python 3.5 llegaría el soporte incluido para entrada/salida asíncrona a través de la biblioteca asyncio, orientada a aplicaciones que requieren alto rendimiento de código concurrente, como servidores web, bibliotecas de conexión de bases de datos y colas de tareas distribuidas.
|
||||
|
||||
@@ -243,7 +269,7 @@ Esta tabla solo proporciona una comparación general entre los lenguajes y que c
|
||||
|
||||
# Agradecimientos 🎁
|
||||
|
||||
Por supuesto, quiero agradecer a [Federico Garay](https://ar.linkedin.com/in/fedegaray) y a [Nicolás Schürmann](https://www.linkedin.com/in/nicolasschurmann/) (¡Cuidao con el [teclado](https://youtu.be/y0T8UqBkawQ) que se gasta!) por los cursos en concreto que realice con ellos, fueron mi despegue. También mi más sincero agradecimiento a todos los contenidos libres de webs, canales de RRSS, repositorios de código, etc.
|
||||
Por supuesto, quiero agradecer a [Federico Garay](https://ar.linkedin.com/in/fedegaray), a [Nicolás Schürmann](https://www.linkedin.com/in/nicolasschurmann/) (¡Cuidao con el [teclado](https://youtu.be/y0T8UqBkawQ) que se gasta!) y a [Marcelo Vázquez](https://www.linkedin.com/in/s4vitar/) por los cursos en concreto que realice con ellos, fueron mi despegue. También mi más sincero agradecimiento a todos los contenidos libres de webs, canales de RRSS, repositorios de código, etc.
|
||||
|
||||
Y por último, a todos los compas que me han apoyado en este camino.
|
||||
|
||||
|
||||
144
catch-all/02_scripts_descifrador_wargame/descifrador_wargame.py
Normal file
144
catch-all/02_scripts_descifrador_wargame/descifrador_wargame.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lo siento, acabo de ver "Juegos de Guerras" y me he emocionado.
|
||||
Este programa descifra un código de números y letras mayúsculas aleatorio.
|
||||
El código se descifra letra por letra y número por número.
|
||||
Se utiliza random para generar los números y letras aleatorias,
|
||||
para que tenga más emoción.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
|
||||
def code_clear():
|
||||
"""
|
||||
Crear un código de números y letras mayúsculas aleatorio
|
||||
"""
|
||||
|
||||
os.system('clear')
|
||||
|
||||
|
||||
def number_generate():
|
||||
"""
|
||||
Genera un número aleatorio entre 0 y 9
|
||||
"""
|
||||
|
||||
number_random = str(random.randint(0, 9))
|
||||
# number_random = chr(random.choice(string.ascii_letters + string.digits))
|
||||
|
||||
return number_random
|
||||
|
||||
|
||||
def upper_letter_generate():
|
||||
"""
|
||||
Genera una letra mayúscula aleatoria entre A y Z
|
||||
"""
|
||||
letter_random = chr(random.randint(65, 90))
|
||||
|
||||
return letter_random
|
||||
|
||||
|
||||
def lower_letter_generate():
|
||||
"""
|
||||
Genera una letra minúscula aleatoria entre a y z
|
||||
"""
|
||||
letter_random = chr(random.randint(97, 122))
|
||||
|
||||
return letter_random
|
||||
|
||||
|
||||
def mecanografiar(msg):
|
||||
for i in range(0, len(msg)):
|
||||
code_clear()
|
||||
print(msg[:i])
|
||||
os.system('sleep .1')
|
||||
|
||||
|
||||
def descifrador(codigo):
|
||||
"""
|
||||
Descifra el código
|
||||
"""
|
||||
|
||||
numero_caracteres = len(codigo)
|
||||
codigo_decode = " " * numero_caracteres
|
||||
while True:
|
||||
|
||||
for i in range(0, len(codigo)):
|
||||
|
||||
if codigo[i] == codigo_decode[i]:
|
||||
|
||||
codigo_decode = codigo_decode[:i] + \
|
||||
codigo[i] + codigo_decode[i + 1:]
|
||||
|
||||
elif codigo[i].isalpha():
|
||||
|
||||
if codigo[i].isupper():
|
||||
|
||||
codigo_decode = codigo_decode[:i] + \
|
||||
upper_letter_generate() + codigo_decode[i + 1:]
|
||||
else:
|
||||
codigo_decode = codigo_decode[:i] + \
|
||||
lower_letter_generate() + codigo_decode[i + 1:]
|
||||
|
||||
elif codigo[i].isdigit():
|
||||
|
||||
codigo_decode = codigo_decode[:i] + \
|
||||
number_generate() + codigo_decode[i + 1:]
|
||||
|
||||
else:
|
||||
|
||||
codigo_decode[i] = " "
|
||||
|
||||
code_clear()
|
||||
print(codigo_decode)
|
||||
os.system('sleep .1')
|
||||
if codigo_decode == codigo:
|
||||
break
|
||||
|
||||
code_clear()
|
||||
return f"El código descifrado es: {codigo_decode}"
|
||||
|
||||
|
||||
def mensaje_final():
|
||||
|
||||
os.system('sleep 3')
|
||||
mecanografiar("Los mísiles nucleares se lanzarán en 5 segundos. ")
|
||||
os.system('sleep 1')
|
||||
|
||||
for i in range(5, 0, -1):
|
||||
code_clear()
|
||||
print(i)
|
||||
os.system('sleep 1')
|
||||
|
||||
mecanografiar("Los mísiles nucleares se han lanzado. ")
|
||||
os.system('sleep 3')
|
||||
|
||||
mecanografiar("Los mísiles nucleares han impactado. ")
|
||||
os.system('sleep 1.5')
|
||||
|
||||
mecanografiar("La humanidad ha sido destruida. ")
|
||||
os.system('sleep 1.5')
|
||||
|
||||
mecanografiar("Fin del programa. ")
|
||||
os.system('sleep 1.5')
|
||||
|
||||
mecanografiar("¡Hasta la vista, baby! 💋💋💋 ")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Función principal
|
||||
"""
|
||||
|
||||
code_clear()
|
||||
print(("#"*36) + "\nBIENVENIDO AL DESCIFRADOR DE CÓDIGOS\n" + ("#"*36))
|
||||
CODIGO = input("Introduzca el código a descifrar: ")
|
||||
|
||||
print(descifrador(CODIGO))
|
||||
|
||||
mensaje_final()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
214
catch-all/03_clima/consulta_tiempo.py
Normal file
214
catch-all/03_clima/consulta_tiempo.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# /var/bin/env python3
|
||||
"""
|
||||
Utilizo esta web:
|
||||
https://www.el-tiempo.net/api
|
||||
|
||||
Contiene una serie de APIs para consultar el tiempo.
|
||||
- Escoger provincia:
|
||||
https://www.el-tiempo.net/api/json/v2/provincias/[CODPROV]
|
||||
- Lista municipio:
|
||||
https://www.el-tiempo.net/api/json/v2/provincias/[CODPROV]/municipios
|
||||
- Escoger municipio:
|
||||
https://www.el-tiempo.net/api/json/v2/provincias/[CODPROV]/municipios/[ID]
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from termcolor import colored
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print(colored("\n\n[!] Saliendo...\n", "red"))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
|
||||
# CONSTANTES
|
||||
SPAIN_URL = "https://www.el-tiempo.net/api/json/v2/home"
|
||||
PROVINCIAS_URL = "https://www.el-tiempo.net/api/json/v2/provincias"
|
||||
|
||||
|
||||
def limpiar_pantalla():
|
||||
"""
|
||||
Limpiar la pantalla
|
||||
"""
|
||||
|
||||
os.system("clear")
|
||||
|
||||
|
||||
def pausa():
|
||||
"""
|
||||
Esperar 2 segundos
|
||||
"""
|
||||
|
||||
os.system("sleep 2")
|
||||
|
||||
|
||||
def request_url(url):
|
||||
"""
|
||||
Realizar una petición GET a una URL y devolver el JSON
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
r = requests.get(url)
|
||||
r.raise_for_status() # Excepción de códigos de estado HTTP no exitosos
|
||||
|
||||
return r.json()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
|
||||
print(colored(f"[!] Error al obtener los datos de {url}: {e}", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def extraer_provincias_data():
|
||||
"""
|
||||
Extraer todas las provincias disponibles
|
||||
"""
|
||||
|
||||
dic_id_codprov_provincias = {}
|
||||
|
||||
provincias_data_request = request_url(PROVINCIAS_URL)
|
||||
|
||||
provincias_data = provincias_data_request["provincias"]
|
||||
|
||||
for i, provincia_data in enumerate(provincias_data):
|
||||
|
||||
id = i+1
|
||||
codprov = provincia_data["CODPROV"]
|
||||
provincia = provincia_data["NOMBRE_PROVINCIA"]
|
||||
dic_id_codprov_provincias[id] = {
|
||||
'codprov': codprov, 'provincia': provincia
|
||||
}
|
||||
|
||||
print(colored(f"{id} - {provincia_data['NOMBRE_PROVINCIA']}", "cyan"))
|
||||
|
||||
return dic_id_codprov_provincias
|
||||
|
||||
|
||||
def seleccionar_provincia(dic_id_codprov_provincias):
|
||||
"""
|
||||
Seleccionar una provincia
|
||||
"""
|
||||
|
||||
prov_selec = input(colored(
|
||||
"[+] Selecciona el número de una provincia: ", "magenta"
|
||||
))
|
||||
|
||||
nombre_prov_selec = dic_id_codprov_provincias[int(prov_selec)]['provincia']
|
||||
|
||||
print(colored(
|
||||
f"\n[+] Has seleccionado la provincia: {nombre_prov_selec}\n", "green")
|
||||
)
|
||||
|
||||
pausa()
|
||||
limpiar_pantalla()
|
||||
|
||||
return dic_id_codprov_provincias[int(prov_selec)]['codprov']
|
||||
|
||||
|
||||
def extrar_municipios_data(cod_prov):
|
||||
"""
|
||||
Extraer todos los municipios de una provincia
|
||||
"""
|
||||
|
||||
dic_id_codmun_municipio = {}
|
||||
|
||||
municipios_url = f"{PROVINCIAS_URL}/{cod_prov}/municipios"
|
||||
|
||||
municipios_data_request = request_url(municipios_url)
|
||||
|
||||
municipios_data = municipios_data_request["municipios"]
|
||||
|
||||
for i, municipio_data in enumerate(municipios_data):
|
||||
|
||||
id = i+1
|
||||
codmun = municipio_data["CODIGOINE"][0:5]
|
||||
municipio = municipio_data['NOMBRE']
|
||||
dic_id_codmun_municipio[id] = {
|
||||
'codmun': codmun, 'municipio': municipio
|
||||
}
|
||||
|
||||
print(colored(f"{id} - {municipio_data['NOMBRE']}", "cyan"))
|
||||
|
||||
return dic_id_codmun_municipio
|
||||
|
||||
|
||||
def seleccionar_municipio(dic_id_codmun_municipio):
|
||||
|
||||
mun_selec = input(colored(
|
||||
"[+] Selecciona el número de un municipio: ", "magenta"
|
||||
))
|
||||
|
||||
nombre_mun_selec = dic_id_codmun_municipio[int(mun_selec)]['municipio']
|
||||
|
||||
print(colored(
|
||||
f"\n[+] Has seleccionado el municipio: {nombre_mun_selec}\n", "green"
|
||||
))
|
||||
|
||||
pausa()
|
||||
limpiar_pantalla()
|
||||
|
||||
return dic_id_codmun_municipio[int(mun_selec)]['codmun']
|
||||
|
||||
|
||||
def info_tiempo(cod_prov, cod_mun):
|
||||
|
||||
url_tiempo = f"{PROVINCIAS_URL}/{cod_prov}/municipios/{cod_mun}"
|
||||
|
||||
tiempo_data_request = request_url(url_tiempo)
|
||||
|
||||
titulo = tiempo_data_request["metadescripcion"]
|
||||
fecha = tiempo_data_request["fecha"]
|
||||
|
||||
hora_amanecer = tiempo_data_request["pronostico"]["hoy"]["@attributes"]["orto"]
|
||||
hora_ocaso = tiempo_data_request["pronostico"]["hoy"]["@attributes"]["ocaso"]
|
||||
|
||||
estado_cielo = tiempo_data_request["stateSky"]["description"]
|
||||
|
||||
temp_actual = tiempo_data_request["temperatura_actual"]
|
||||
temp_min = tiempo_data_request["temperaturas"]["min"]
|
||||
temp_max = tiempo_data_request["temperaturas"]["max"]
|
||||
|
||||
humedad = tiempo_data_request["humedad"]
|
||||
|
||||
print(colored(f"\n[+] {titulo.strip().upper()} A FECHA {fecha}\n", "blue"))
|
||||
|
||||
print(colored(f"[+] Hora de amanecer: {hora_amanecer}", "blue"))
|
||||
print(colored(f"[+] Hora de ocaso: {hora_ocaso}\n", "blue"))
|
||||
|
||||
print(colored(f"[+] Estado del cielo: {estado_cielo}\n", "blue"))
|
||||
|
||||
print(colored(f"[+] Temperatura actual: {temp_actual}ºC", "blue"))
|
||||
print(colored(f"[+] Temperatura mínima: {temp_min}ºC", "blue"))
|
||||
print(colored(f"[+] Temperatura máxima: {temp_max}ºC\n", "blue"))
|
||||
|
||||
print(colored(f"[+] Humedad: {humedad}%\n", "blue"))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
limpiar_pantalla()
|
||||
|
||||
dic_id_codprov_provincias = extraer_provincias_data()
|
||||
|
||||
cod_prov = seleccionar_provincia(dic_id_codprov_provincias)
|
||||
|
||||
dic_id_codmun_municipio = extrar_municipios_data(cod_prov)
|
||||
|
||||
cod_mun = seleccionar_municipio(dic_id_codmun_municipio)
|
||||
|
||||
info_tiempo(cod_prov, cod_mun)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
main()
|
||||
107
catch-all/04_acortador_url/acortar.py
Normal file
107
catch-all/04_acortador_url/acortar.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Acortador de enlaces
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import regex
|
||||
import secrets
|
||||
import string
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from termcolor import colored
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
|
||||
print(colored('\n[!] Saliendo...', 'red'))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
|
||||
def generar_codigo():
|
||||
|
||||
# Definir caracteres para cadena aleatoria
|
||||
alfanumerica = string.ascii_letters + string.digits
|
||||
|
||||
# Generar cadena aleatoria
|
||||
codigo = ''.join(secrets.choice(alfanumerica) for i in range(8))
|
||||
|
||||
return codigo
|
||||
|
||||
|
||||
def acortar_url(url):
|
||||
|
||||
codigo = generar_codigo()
|
||||
data = []
|
||||
|
||||
with open('codigos.json', 'r') as f:
|
||||
|
||||
data = json.load(f)
|
||||
|
||||
codigo_existe = any(item['codigo'] == codigo for item in data)
|
||||
|
||||
if codigo_existe:
|
||||
codigo = generar_codigo()
|
||||
|
||||
with open('codigos.json', 'w') as f:
|
||||
|
||||
data.append(
|
||||
{
|
||||
'codigo': codigo,
|
||||
'url': f"http://localhost:5000/{codigo}",
|
||||
'redireccion': url,
|
||||
}
|
||||
)
|
||||
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
|
||||
def comprobar_url(url):
|
||||
|
||||
# Comprobar si la URL tiene http o https
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = f'https://{url}'
|
||||
|
||||
if url.endswith('/'):
|
||||
url = url[:-1]
|
||||
|
||||
# Comprobar formato de URL con los patrones
|
||||
if regex.match(r'^https?://[\w.-]+\.[\w.-]+(/[\w.-]+)*$', url):
|
||||
|
||||
return url
|
||||
|
||||
else:
|
||||
|
||||
raise ValueError(colored('URL no válida', 'red'))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description='Acortador de URL')
|
||||
parser.add_argument('-u', '--url', help='URL a acortar')
|
||||
args = parser.parse_args()
|
||||
url = args.url
|
||||
|
||||
if not url:
|
||||
|
||||
url = input(colored('Introduce la URL a acortar: ', 'cyan'))
|
||||
|
||||
url = comprobar_url(url)
|
||||
|
||||
acortar_url(url)
|
||||
|
||||
except argparse.ArgumentError as e:
|
||||
print(colored(f'[!] Error en los argumentos: {e}', 'red'))
|
||||
|
||||
except Exception as e:
|
||||
print(colored(f'[!] Error en la ejecución principal: {e}', 'red'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
main()
|
||||
32
catch-all/04_acortador_url/app.py
Normal file
32
catch-all/04_acortador_url/app.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Redirecciona a una URL a partir de un código
|
||||
"""
|
||||
|
||||
import os
|
||||
from flask import Flask, redirect
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/<string:codigo>')
|
||||
def redireccion(codigo: str):
|
||||
|
||||
data = []
|
||||
|
||||
with open('codigos.json', 'r') as f:
|
||||
|
||||
data = json.load(f)
|
||||
|
||||
r = list(filter(lambda x: x['codigo'] == codigo, data))
|
||||
|
||||
if r:
|
||||
return redirect(r[0]['redireccion'], code=302)
|
||||
|
||||
return {
|
||||
'message': 'Código no encontrado'
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
22
catch-all/04_acortador_url/codigos.json
Normal file
22
catch-all/04_acortador_url/codigos.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"codigo": "",
|
||||
"url": "",
|
||||
"redireccion": ""
|
||||
},
|
||||
{
|
||||
"codigo": "NA1w9oky",
|
||||
"url": "http://localhost:5000/NA1w9oky",
|
||||
"redireccion": "https://vergaracarmona.es"
|
||||
},
|
||||
{
|
||||
"codigo": "XuI6ysnl",
|
||||
"url": "http://localhost:5000/XuI6ysnl",
|
||||
"redireccion": "https://www.linkedin.com/in/manu-vergara"
|
||||
},
|
||||
{
|
||||
"codigo": "uMsW2vzQ",
|
||||
"url": "http://localhost:5000/uMsW2vzQ",
|
||||
"redireccion": "https://gitea.vergaracarmona.es/manudocker"
|
||||
}
|
||||
]
|
||||
90
catch-all/05_infra_test/01_redis_flask_docker/.dockerignore
Normal file
90
catch-all/05_infra_test/01_redis_flask_docker/.dockerignore
Normal file
@@ -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/
|
||||
12
catch-all/05_infra_test/01_redis_flask_docker/Dockerfile
Normal file
12
catch-all/05_infra_test/01_redis_flask_docker/Dockerfile
Normal file
@@ -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"]
|
||||
|
||||
188
catch-all/05_infra_test/01_redis_flask_docker/README.md
Normal file
188
catch-all/05_infra_test/01_redis_flask_docker/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Crear una API Caching con Redis, Flask y Docker
|
||||
|
||||
<!-- Artículo original: https://dev.to/vjanz/implement-api-caching-with-redis-flask-and-docker-step-by-step-5h01 -->
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
|
||||
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 🚀
|
||||
|
||||
21
catch-all/05_infra_test/01_redis_flask_docker/app.py
Normal file
21
catch-all/05_infra_test/01_redis_flask_docker/app.py
Normal file
@@ -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')
|
||||
11
catch-all/05_infra_test/01_redis_flask_docker/config.py
Normal file
11
catch-all/05_infra_test/01_redis_flask_docker/config.py
Normal file
@@ -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']
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
container_name: app-python-flask-with-redis
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- '5000:5000'
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
redis:
|
||||
image: redis:7.0-alpine
|
||||
container_name: redis-python
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
# Establecer la conexión con el servidor RabbitMQ
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters('localhost'))
|
||||
channel = connection.channel()
|
||||
|
||||
# Asegurarnos de que la cola existe
|
||||
try:
|
||||
channel.queue_declare(queue='hola')
|
||||
except pika.exceptions.ChannelClosedByBroker:
|
||||
print(' [!] Error al crear la cola. ¿Está el servidor RabbitMQ corriendo?')
|
||||
sys.exit(1)
|
||||
except e:
|
||||
print(f' [!] Error: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Recibir mensajes de la cola es un poco más complejo que enviarlos.
|
||||
# Funciona suscribiendo una función callback a una cola.
|
||||
# Cada vez que recibimos un mensaje, esta función callback es llamada por la
|
||||
# librería Pika.
|
||||
# En nuestro caso esta función imprimirá en pantalla el contenido del mensaje.
|
||||
def callback(ch, method, properties, body):
|
||||
print(f" [+] Recibido \"{body.decode()}\"")
|
||||
|
||||
# Ahora indicamos a RabbitMQ que comience a consumir mensajes de la cola.
|
||||
channel.basic_consume(
|
||||
queue='hola', auto_ack=True, on_message_callback=callback
|
||||
)
|
||||
|
||||
# Bucle infinito que espera mensajes de la cola y llama a la función callback
|
||||
print(' [i] Esperando mensajes. Para salir presiona CTRL+C')
|
||||
channel.start_consuming()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print(' [!] Saliendo')
|
||||
try:
|
||||
sys.exit(0)
|
||||
except SystemExit:
|
||||
os._exit(0)
|
||||
20
catch-all/05_infra_test/02_rabbitmq/01_hello_world/send.py
Normal file
20
catch-all/05_infra_test/02_rabbitmq/01_hello_world/send.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
|
||||
# Establecer la conexión con el servidor RabbitMQ
|
||||
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
|
||||
channel = connection.channel()
|
||||
|
||||
# Crear una cola llamada 'hola'
|
||||
channel.queue_declare(queue='hola')
|
||||
|
||||
# Enviar mensaje
|
||||
channel.basic_publish(
|
||||
exchange='', routing_key='hola', body='¡Hola Mundo!'
|
||||
)
|
||||
|
||||
# Traza del envío
|
||||
print(" [+] Enviado 'Hola Mundo!'")
|
||||
|
||||
# Cerrar la conexión
|
||||
connection.close()
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import sys
|
||||
|
||||
|
||||
# Establecer la conexión con el servidor RabbitMQ
|
||||
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
|
||||
channel = connection.channel()
|
||||
|
||||
# Crear una cola llamada 'hola'
|
||||
channel.queue_declare(queue='task_queue', durable=True)
|
||||
|
||||
# Mensaje a enviar
|
||||
message = ' '.join(sys.argv[1:]) or "¡Hola mundo!"
|
||||
|
||||
# Enviar mensaje
|
||||
channel.basic_publish(
|
||||
exchange='', routing_key='task_queue', body=message,
|
||||
properties=pika.BasicProperties(
|
||||
delivery_mode=pika.DeliveryMode.Persistent
|
||||
)
|
||||
)
|
||||
|
||||
# Traza del envío
|
||||
print(f" [+] Enviado '{message}'")
|
||||
|
||||
# Cerrar la conexión
|
||||
connection.close()
|
||||
50
catch-all/05_infra_test/02_rabbitmq/02_work_queues/worker.py
Normal file
50
catch-all/05_infra_test/02_rabbitmq/02_work_queues/worker.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
# Establecer la conexión con el servidor RabbitMQ
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters('localhost'))
|
||||
channel = connection.channel()
|
||||
|
||||
# Comprobar si la cola existe
|
||||
try:
|
||||
channel.queue_declare(queue='task_queue', durable=True)
|
||||
except pika.exceptions.ChannelClosedByBroker:
|
||||
print(' [!] Error al crear la cola. ¿Está el servidor RabbitMQ corriendo?')
|
||||
sys.exit(1)
|
||||
except e:
|
||||
print(f' [!] Error: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Consumir mensajes
|
||||
|
||||
def callback(ch, method, properties, body):
|
||||
print(f"[+] Recibido {body.decode()}")
|
||||
time.sleep(body.count(b'.'))
|
||||
print("[i] Hecho")
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
# Consumir mensajes de la cola 'task_queue'
|
||||
channel.basic_qos(prefetch_count=1)
|
||||
channel.basic_consume(queue='task_queue', on_message_callback=callback)
|
||||
|
||||
# Iniciar la escucha
|
||||
print('[i] Esperando mensajes. Para salir presiona CTRL+C')
|
||||
channel.start_consuming()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print(' [!] Saliendo')
|
||||
try:
|
||||
sys.exit(0)
|
||||
except SystemExit:
|
||||
os._exit(0)
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import logging
|
||||
import pika
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
|
||||
|
||||
def main():
|
||||
# Configuración de logging
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Configuración de argparse para manejar argumentos de línea de comandos
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Envía un mensaje al intercambio de logs en RabbitMQ."
|
||||
)
|
||||
parser.add_argument(
|
||||
'message',
|
||||
nargs='*',
|
||||
help='El mensaje a enviar. Si no se especifica, se enviará "Traza de log"'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--host', default='localhost',
|
||||
help='El host de RabbitMQ (default: localhost)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--user', default='invent',
|
||||
help='El usuario de RabbitMQ (default: invent)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--password', default='123456',
|
||||
help='La contraseña de RabbitMQ (default: 123456)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Crear el mensaje base
|
||||
base_message = ' '.join(args.message) or "Traza de log"
|
||||
|
||||
stop_sending = threading.Event()
|
||||
|
||||
def send_messages():
|
||||
credentials = pika.PlainCredentials(args.user, args.password)
|
||||
try:
|
||||
# Establecer conexión con RabbitMQ
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(
|
||||
host=args.host, credentials=credentials)
|
||||
)
|
||||
channel = connection.channel()
|
||||
|
||||
# Declarar el intercambio de tipo 'fanout'
|
||||
channel.exchange_declare(exchange='logs', exchange_type='fanout')
|
||||
|
||||
while not stop_sending.is_set():
|
||||
# Crear mensaje con fecha y hora actual
|
||||
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
num_logs = randint(1, 1000)
|
||||
message = f"{current_time} - CUSTOM_LOG - {base_message}: Número aleatorio: {num_logs}"
|
||||
|
||||
# Publicar el mensaje en el intercambio
|
||||
channel.basic_publish(
|
||||
exchange='logs', routing_key='', body=message)
|
||||
logging.info(f"[+] Sent {message}")
|
||||
|
||||
# Esperar 5 segundos antes de enviar el siguiente mensaje
|
||||
time.sleep(5)
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
logging.error(f"[!] No se pudo conectar a RabbitMQ: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"[!] Ocurrió un error: {e}")
|
||||
finally:
|
||||
# Cerrar la conexión
|
||||
if 'connection' in locals() and connection.is_open:
|
||||
connection.close()
|
||||
|
||||
# Iniciar el hilo que enviará mensajes
|
||||
sender_thread = threading.Thread(target=send_messages)
|
||||
sender_thread.start()
|
||||
|
||||
try:
|
||||
# Esperar a que el usuario introduzca 'q' para detener el envío de mensajes
|
||||
while True:
|
||||
user_input = input()
|
||||
if user_input.strip().lower() == 'q':
|
||||
stop_sending.set()
|
||||
sender_thread.join()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
stop_sending.set()
|
||||
sender_thread.join()
|
||||
logging.info("Interrupción del usuario recibida. Saliendo...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
|
||||
def main():
|
||||
# Configuración de logging
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Configuración de argparse para manejar argumentos de línea de comandos
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Escucha mensajes del intercambio de logs en RabbitMQ."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--host', default='localhost',
|
||||
help='El host de RabbitMQ (default: localhost)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--user', default='invent',
|
||||
help='El usuario de RabbitMQ (default: invent)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--password', default='123456',
|
||||
help='La contraseña de RabbitMQ (default: 123456)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
credentials = pika.PlainCredentials(args.user, args.password)
|
||||
|
||||
try:
|
||||
# Establecer conexión con RabbitMQ
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=args.host, credentials=credentials)
|
||||
)
|
||||
channel = connection.channel()
|
||||
|
||||
# Declarar el intercambio de tipo 'fanout'
|
||||
channel.exchange_declare(exchange='logs', exchange_type='fanout')
|
||||
|
||||
# Declarar una cola exclusiva para el consumidor
|
||||
result = channel.queue_declare(queue='', exclusive=True)
|
||||
queue_name = result.method.queue
|
||||
|
||||
# Enlazar la cola al intercambio de logs
|
||||
channel.queue_bind(exchange='logs', queue=queue_name)
|
||||
|
||||
logging.info(' [*] Waiting for logs. To exit press CTRL+C')
|
||||
|
||||
# Función de callback para manejar mensajes entrantes
|
||||
def callback(ch, method, properties, body):
|
||||
logging.info(f" [x] Received: {body.decode()}")
|
||||
|
||||
# Configurar el consumidor
|
||||
channel.basic_consume(
|
||||
queue=queue_name, on_message_callback=callback, auto_ack=True)
|
||||
|
||||
# Iniciar el bucle de consumo
|
||||
channel.start_consuming()
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
logging.error(f"[!] No se pudo conectar a RabbitMQ: {e}")
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Interrupción del usuario recibida. Saliendo...")
|
||||
except Exception as e:
|
||||
logging.error(f"[!] Ocurrió un error: {e}")
|
||||
finally:
|
||||
# Cerrar la conexión si está abierta
|
||||
if 'connection' in locals() and connection.is_open:
|
||||
connection.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
import random
|
||||
import datetime
|
||||
import threading
|
||||
import signal
|
||||
|
||||
|
||||
def establish_connection(host: str, port: int):
|
||||
"""Establece la conexión con RabbitMQ."""
|
||||
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=host, port=port)
|
||||
)
|
||||
|
||||
return connection
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
print(f"\n[!] Error al conectar con RabbitMQ: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def publish_message(channel, exchange: str, severity: str, message: str):
|
||||
"""Publica un mensaje en el intercambio especificado."""
|
||||
|
||||
try:
|
||||
channel.basic_publish(
|
||||
exchange=exchange,
|
||||
routing_key=severity,
|
||||
body=message
|
||||
)
|
||||
print(f"[i] Sent {severity}:{message}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[!] Error al enviar mensaje: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def send_messages_periodically(channel, exchange_name, severity, base_message):
|
||||
"""Envía mensajes periódicamente cada 5 segundos hasta que se detenga."""
|
||||
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
# Generar número aleatorio para identificar el envío
|
||||
random_number = random.randint(1000, 9999)
|
||||
|
||||
# Obtener la fecha y hora actual
|
||||
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Crear el mensaje con la fecha, hora y número aleatorio
|
||||
message = f"{base_message} [{random_number}] at {current_time}"
|
||||
|
||||
# Publicar el mensaje
|
||||
publish_message(channel, exchange_name, severity, message)
|
||||
|
||||
# Esperar 5 segundos antes de enviar el siguiente mensaje
|
||||
# Comprobación del evento de parada durante la espera
|
||||
for _ in range(50):
|
||||
if stop_event.is_set():
|
||||
break
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def user_input_thread():
|
||||
"""Hilo para capturar la entrada del usuario."""
|
||||
|
||||
while not stop_event.is_set():
|
||||
user_input = input()
|
||||
if user_input.strip().lower() == 'q':
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
"""Manejador de señal para terminar el programa."""
|
||||
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Envía mensajes a RabbitMQ usando un intercambio directo.')
|
||||
parser.add_argument('--host', type=str, default='localhost',
|
||||
help='El host de RabbitMQ (por defecto: localhost)')
|
||||
parser.add_argument('--port', type=int, default=5672,
|
||||
help='El puerto de RabbitMQ (por defecto: 5672)')
|
||||
parser.add_argument('severity', type=str, nargs='?',
|
||||
default='info', help='La severidad del mensaje')
|
||||
parser.add_argument('message', type=str, nargs='*',
|
||||
default=['Hello', 'World!'], help='El mensaje a enviar')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Establecer conexión
|
||||
connection = establish_connection(args.host, args.port)
|
||||
channel = connection.channel()
|
||||
|
||||
# Declarar intercambio
|
||||
exchange_name = 'direct_logs'
|
||||
channel.exchange_declare(exchange=exchange_name, exchange_type='direct')
|
||||
|
||||
# Mensaje base
|
||||
base_message = ' '.join(args.message)
|
||||
|
||||
# Crear un hilo para el envío periódico de mensajes
|
||||
send_thread = threading.Thread(target=send_messages_periodically, args=(
|
||||
channel, exchange_name, args.severity, base_message))
|
||||
send_thread.start()
|
||||
|
||||
# Crear un hilo para capturar la entrada del usuario
|
||||
input_thread = threading.Thread(target=user_input_thread)
|
||||
input_thread.start()
|
||||
|
||||
print("Presiona 'q' para detener el envío de mensajes.")
|
||||
|
||||
# Esperar a que los hilos terminen antes de cerrar la conexión
|
||||
send_thread.join()
|
||||
input_thread.join()
|
||||
|
||||
# Cerrar conexión
|
||||
connection.close()
|
||||
print("Conexión cerrada. Programa terminado.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# Crear un evento para detener el envío de mensajes
|
||||
stop_event = threading.Event()
|
||||
|
||||
# Configurar el manejador de señales para terminar el programa
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import sys
|
||||
import argparse
|
||||
import signal
|
||||
|
||||
|
||||
def establish_connection(host: str, port: int):
|
||||
"""Establece la conexión con RabbitMQ."""
|
||||
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=host, port=port)
|
||||
)
|
||||
|
||||
return connection
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
print(f"\n[!] Error al conectar con RabbitMQ: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def declare_exchange_and_queue(channel, exchange_name: str, severities: list):
|
||||
"""Declara el intercambio y las colas necesarias."""
|
||||
|
||||
channel.exchange_declare(exchange=exchange_name, exchange_type='direct')
|
||||
|
||||
# Crear una cola exclusiva
|
||||
result = channel.queue_declare(queue='', exclusive=True)
|
||||
queue_name = result.method.queue
|
||||
|
||||
# Vincular la cola al intercambio para cada severidad especificada
|
||||
for severity in severities:
|
||||
channel.queue_bind(
|
||||
exchange=exchange_name, queue=queue_name, routing_key=severity)
|
||||
|
||||
return queue_name
|
||||
|
||||
|
||||
def callback(ch, method, properties, body):
|
||||
"""Función de callback para procesar mensajes recibidos."""
|
||||
|
||||
print(f" [i] {method.routing_key}:{body.decode()}")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Recibe mensajes de RabbitMQ usando un intercambio directo.')
|
||||
parser.add_argument('--host', type=str, default='localhost',
|
||||
help='El host de RabbitMQ (por defecto: localhost)')
|
||||
parser.add_argument('--port', type=int, default=5672,
|
||||
help='El puerto de RabbitMQ (por defecto: 5672)')
|
||||
parser.add_argument('severities', metavar='S', type=str, nargs='+',
|
||||
help='Lista de severidades a recibir (info, warning, error)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Establecer conexión
|
||||
connection = establish_connection(args.host, args.port)
|
||||
channel = connection.channel()
|
||||
|
||||
# Declarar el intercambio y las colas
|
||||
exchange_name = 'direct_logs'
|
||||
queue_name = declare_exchange_and_queue(
|
||||
channel, exchange_name, args.severities)
|
||||
|
||||
print('\n[!] Esperando logs. Para salir presionar CTRL+C')
|
||||
|
||||
# Iniciar el consumo de mensajes
|
||||
channel.basic_consume(
|
||||
queue=queue_name, on_message_callback=callback, auto_ack=True)
|
||||
|
||||
# Manejar Ctrl+C para detener el programa
|
||||
try:
|
||||
channel.start_consuming()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupción recibida. Cerrando conexión...")
|
||||
connection.close()
|
||||
print("\n[!] Conexión cerrada. Programa terminado.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
114
catch-all/05_infra_test/02_rabbitmq/05_topics/emit_log_topic.py
Normal file
114
catch-all/05_infra_test/02_rabbitmq/05_topics/emit_log_topic.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
# Configuración del logger
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""
|
||||
Analiza los argumentos de línea de comandos utilizando argparse.
|
||||
Devuelve un objeto con los argumentos proporcionados por el usuario.
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Enviar mensajes a un intercambio de tipo "topic" en RabbitMQ.')
|
||||
parser.add_argument(
|
||||
'routing_key', help='La clave de enrutamiento para el mensaje.')
|
||||
parser.add_argument(
|
||||
'message', nargs='*', default=['Hola', 'Mundo!'], help='El mensaje base a enviar.')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def establish_connection():
|
||||
"""
|
||||
Establece una conexión con RabbitMQ.
|
||||
Retorna el objeto de conexión si es exitoso.
|
||||
Salida del programa si hay un error de conexión.
|
||||
"""
|
||||
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host='localhost'))
|
||||
|
||||
return connection
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
logging.error('\n[!] Error al conectar con RabbitMQ: %s', e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def declare_exchange(channel):
|
||||
"""
|
||||
Declara el intercambio de tipo 'topic'.
|
||||
Salida del programa si hay un error al declarar el intercambio.
|
||||
"""
|
||||
|
||||
try:
|
||||
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
|
||||
|
||||
except pika.exceptions.ChannelError as e:
|
||||
logging.error('\n[!] Error al declarar el intercambio: %s', e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def generate_message(base_message):
|
||||
"""
|
||||
Genera un mensaje único que incluye un número aleatorio, fecha y hora actual.
|
||||
"""
|
||||
|
||||
random_id = random.randint(
|
||||
1000, 9999) # Genera un ID aleatorio de 4 dígitos
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # Fecha y hora actual
|
||||
|
||||
return f"{timestamp} - ({random_id}): {base_message}"
|
||||
|
||||
|
||||
def publish_message(channel, routing_key, message):
|
||||
"""
|
||||
Publica un mensaje en el intercambio declarado.
|
||||
"""
|
||||
|
||||
channel.basic_publish(exchange='topic_logs',
|
||||
routing_key=routing_key, body=message)
|
||||
logging.info(' [+] Enviado %s:%s', routing_key, message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Función principal que orquesta la ejecución del script.
|
||||
"""
|
||||
|
||||
# Parsear los argumentos de línea de comandos
|
||||
args = parse_arguments()
|
||||
routing_key = args.routing_key
|
||||
base_message = ' '.join(args.message)
|
||||
|
||||
# Establecer conexión y publicar mensaje cada 5 segundos
|
||||
with establish_connection() as connection:
|
||||
channel = connection.channel()
|
||||
declare_exchange(channel)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Generar y publicar el mensaje
|
||||
message = generate_message(base_message)
|
||||
publish_message(channel, routing_key, message)
|
||||
# Espera de 5 segundos antes de enviar el siguiente mensaje
|
||||
time.sleep(5)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("\n[!] Interrupción del usuario. Terminando...")
|
||||
except Exception as e:
|
||||
logging.error("\n[!] Se produjo un error inesperado: %s", e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# Configuración del logger
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""
|
||||
Analiza los argumentos de línea de comandos utilizando argparse.
|
||||
Devuelve un objeto con los argumentos proporcionados por el usuario.
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Recibe mensajes de un intercambio de tipo "topic" en RabbitMQ.')
|
||||
parser.add_argument('binding_keys', nargs='+',
|
||||
help='Lista de claves de enlace para filtrar los mensajes.')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def establish_connection():
|
||||
"""
|
||||
Establece una conexión con RabbitMQ.
|
||||
Retorna el objeto de conexión si es exitoso.
|
||||
Salida del programa si hay un error de conexión.
|
||||
"""
|
||||
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host='localhost'))
|
||||
return connection
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
logging.error('[!] Error al conectar con RabbitMQ: %s', e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def declare_exchange_and_queue(channel):
|
||||
"""
|
||||
Declara el intercambio de tipo 'topic' y una cola exclusiva.
|
||||
Retorna el nombre de la cola creada.
|
||||
"""
|
||||
|
||||
try:
|
||||
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
|
||||
result = channel.queue_declare('', exclusive=True)
|
||||
|
||||
return result.method.queue
|
||||
|
||||
except pika.exceptions.ChannelError as e:
|
||||
logging.error('[!] Error al declarar el intercambio o la cola: %s', e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def bind_queue(channel, queue_name, binding_keys):
|
||||
"""
|
||||
Vincula la cola al intercambio con las claves de enlace proporcionadas.
|
||||
"""
|
||||
|
||||
for binding_key in binding_keys:
|
||||
try:
|
||||
channel.queue_bind(exchange='topic_logs',
|
||||
queue=queue_name, routing_key=binding_key)
|
||||
logging.info(
|
||||
' [i] Cola vinculada con clave de enlace: %s', binding_key)
|
||||
|
||||
except pika.exceptions.ChannelError as e:
|
||||
logging.error(
|
||||
'[!] Error al vincular la cola con la clave de enlace %s: %s', binding_key, e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def callback(ch, method, properties, body):
|
||||
"""
|
||||
Función de callback que maneja los mensajes recibidos.
|
||||
"""
|
||||
|
||||
logging.info(' [+] %s: %s', method.routing_key.upper(), body.decode())
|
||||
|
||||
|
||||
def start_consuming(channel, queue_name):
|
||||
"""
|
||||
Inicia la recepción de mensajes desde la cola especificada.
|
||||
"""
|
||||
|
||||
channel.basic_consume(
|
||||
queue=queue_name, on_message_callback=callback, auto_ack=True)
|
||||
logging.info(' [i] Esperando mensajes. Para salir presione CTRL+C')
|
||||
|
||||
try:
|
||||
channel.start_consuming()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info(' [!] Interrupción del usuario. Terminando...')
|
||||
channel.stop_consuming()
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Función principal que orquesta la ejecución del script.
|
||||
"""
|
||||
|
||||
# Parsear los argumentos de línea de comandos
|
||||
args = parse_arguments()
|
||||
binding_keys = args.binding_keys
|
||||
|
||||
# Establecer conexión, declarar intercambio y cola, vincular y comenzar a consumir
|
||||
with establish_connection() as connection:
|
||||
channel = connection.channel()
|
||||
queue_name = declare_exchange_and_queue(channel)
|
||||
bind_queue(channel, queue_name, binding_keys)
|
||||
start_consuming(channel, queue_name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
113
catch-all/05_infra_test/02_rabbitmq/06_rpc/rpc_client.py
Normal file
113
catch-all/05_infra_test/02_rabbitmq/06_rpc/rpc_client.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
import uuid
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
class FibonacciRpcClient:
|
||||
def __init__(self, host='localhost'):
|
||||
# Establece la conexión con RabbitMQ
|
||||
try:
|
||||
self.connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=host)
|
||||
)
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
print(f"[!] Error al conectar con RabbitMQ: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Crea un canal de comunicación
|
||||
self.channel = self.connection.channel()
|
||||
|
||||
# Declara una cola exclusiva para recibir las respuestas del servidor RPC
|
||||
result = self.channel.queue_declare(queue='', exclusive=True)
|
||||
self.callback_queue = result.method.queue
|
||||
|
||||
# Configura el canal para consumir mensajes de la cola de respuestas
|
||||
self.channel.basic_consume(
|
||||
queue=self.callback_queue,
|
||||
on_message_callback=self.on_response,
|
||||
auto_ack=True
|
||||
)
|
||||
|
||||
# Inicializa las variables para manejar la respuesta RPC
|
||||
self.response = None
|
||||
self.corr_id = None
|
||||
|
||||
def on_response(self, ch, method, props, body):
|
||||
"""Callback ejecutado al recibir una respuesta del servidor RPC."""
|
||||
# Verifica si el ID de correlación coincide con el esperado
|
||||
if self.corr_id == props.correlation_id:
|
||||
self.response = body
|
||||
|
||||
def call(self, n):
|
||||
"""
|
||||
Envía una solicitud RPC para calcular el número de Fibonacci de n.
|
||||
|
||||
:param n: Número entero no negativo para calcular su Fibonacci.
|
||||
:return: Resultado del cálculo de Fibonacci.
|
||||
:raises ValueError: Si n no es un número entero no negativo.
|
||||
"""
|
||||
# Verifica que la entrada sea un número entero no negativo
|
||||
if not isinstance(n, int) or n < 0:
|
||||
raise ValueError("[!] El argumento debe ser un entero no negativo.")
|
||||
|
||||
# Resetea la respuesta y genera un nuevo ID de correlación único
|
||||
self.response = None
|
||||
self.corr_id = str(uuid.uuid4())
|
||||
|
||||
# Publica un mensaje en la cola 'rpc_queue' con las propiedades necesarias
|
||||
self.channel.basic_publish(
|
||||
exchange='',
|
||||
routing_key='rpc_queue',
|
||||
properties=pika.BasicProperties(
|
||||
reply_to=self.callback_queue, # Cola de retorno para recibir la respuesta
|
||||
correlation_id=self.corr_id, # ID único para correlacionar la respuesta
|
||||
),
|
||||
body=str(n)
|
||||
)
|
||||
|
||||
# Espera hasta que se reciba la respuesta del servidor
|
||||
while self.response is None:
|
||||
self.connection.process_data_events(time_limit=None)
|
||||
|
||||
# Devuelve la respuesta convertida a un entero
|
||||
return int(self.response)
|
||||
|
||||
def close(self):
|
||||
"""Cierra la conexión con el servidor de RabbitMQ."""
|
||||
self.connection.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Configura el analizador de argumentos para recibir el número de Fibonacci
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Calcula números de Fibonacci mediante RPC.")
|
||||
parser.add_argument(
|
||||
"number",
|
||||
type=int,
|
||||
help="Número entero no negativo para calcular su Fibonacci."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Inicializa el cliente RPC de Fibonacci
|
||||
fibonacci_rpc = FibonacciRpcClient()
|
||||
|
||||
# Número para el cual se desea calcular el Fibonacci
|
||||
n = args.number
|
||||
|
||||
try:
|
||||
print(f" [+] Solicitando fib({n})")
|
||||
# Realiza la llamada RPC y obtiene el resultado
|
||||
response = fibonacci_rpc.call(n)
|
||||
print(f" [+] Resultado fib({n}) = {response}")
|
||||
|
||||
except ValueError as e:
|
||||
print(f"[!] Error de valor: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[!] Error inesperado: {e}")
|
||||
|
||||
finally:
|
||||
# Cierra la conexión del cliente RPC
|
||||
fibonacci_rpc.close()
|
||||
92
catch-all/05_infra_test/02_rabbitmq/06_rpc/rpc_server.py
Normal file
92
catch-all/05_infra_test/02_rabbitmq/06_rpc/rpc_server.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
import pika
|
||||
from functools import lru_cache
|
||||
import time
|
||||
|
||||
# Establecemos la conexión a RabbitMQ
|
||||
|
||||
|
||||
def create_connection():
|
||||
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host='localhost')
|
||||
)
|
||||
|
||||
return connection
|
||||
|
||||
except pika.exceptions.AMQPConnectionError as e:
|
||||
print(f"[!] Error al conectar a RabbitMQ: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Conexión a RabbitMQ
|
||||
connection = create_connection()
|
||||
|
||||
if not connection:
|
||||
exit(1)
|
||||
|
||||
# Canal de comunicación con RabbitMQ
|
||||
channel = connection.channel()
|
||||
|
||||
# Declaración de la cola 'rpc_queue'
|
||||
channel.queue_declare(queue='rpc_queue')
|
||||
|
||||
# Implementación mejorada de Fibonacci con memoización
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def fib(n):
|
||||
|
||||
if n < 0:
|
||||
raise ValueError("\n[!] El número no puede ser negativo.")
|
||||
|
||||
elif n == 0:
|
||||
return 0
|
||||
|
||||
elif n == 1:
|
||||
return 1
|
||||
|
||||
else:
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
|
||||
# Callback para procesar solicitudes RPC
|
||||
|
||||
|
||||
def on_request(ch, method, props, body):
|
||||
|
||||
try:
|
||||
n = int(body)
|
||||
print(f" [+] Calculando fib({n})")
|
||||
|
||||
# Simula un tiempo de procesamiento de 2 segundos
|
||||
time.sleep(2)
|
||||
|
||||
response = fib(n)
|
||||
print(f" [+] Resultado: {response}")
|
||||
|
||||
except ValueError as e:
|
||||
response = f"\n[!] Error: {str(e)}"
|
||||
|
||||
except Exception as e:
|
||||
response = f"\n[!] Error inesperado: {str(e)}"
|
||||
|
||||
# Publicar la respuesta al cliente
|
||||
ch.basic_publish(
|
||||
exchange='',
|
||||
routing_key=props.reply_to,
|
||||
properties=pika.BasicProperties(correlation_id=props.correlation_id),
|
||||
body=str(response)
|
||||
)
|
||||
|
||||
# Confirmar la recepción del mensaje
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
|
||||
# Configuración de calidad de servicio y consumo de mensajes
|
||||
channel.basic_qos(prefetch_count=1)
|
||||
channel.basic_consume(queue='rpc_queue', on_message_callback=on_request)
|
||||
|
||||
print("\n[i] Esperando solicitudes RPC")
|
||||
channel.start_consuming()
|
||||
462
catch-all/05_infra_test/02_rabbitmq/README.md
Normal file
462
catch-all/05_infra_test/02_rabbitmq/README.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# Pruebas con rabbitmq
|
||||
|
||||
*Índice de contenidos:*
|
||||
- [Pruebas con rabbitmq](#pruebas-con-rabbitmq)
|
||||
- [Despliegue rabbitmq con docker](#despliegue-rabbitmq-con-docker)
|
||||
- [Pruebas](#pruebas)
|
||||
- [Hello World](#hello-world)
|
||||
- [Work Queues](#work-queues)
|
||||
- [Publish/Subscribe](#publishsubscribe)
|
||||
- [Routing](#routing)
|
||||
- [Enlaces](#enlaces)
|
||||
- [Intercambio Directo](#intercambio-directo)
|
||||
- [Múltiples Enlaces](#múltiples-enlaces)
|
||||
- [Emisión de Logs](#emisión-de-logs)
|
||||
- [Suscripción](#suscripción)
|
||||
- [Código de Ejemplo](#código-de-ejemplo)
|
||||
- [Ejemplos de Uso](#ejemplos-de-uso)
|
||||
- [Topics (Próximamente)](#topics-próximamente)
|
||||
- [¿Qué es un intercambio de temas?](#qué-es-un-intercambio-de-temas)
|
||||
- [Casos especiales de `binding_key`](#casos-especiales-de-binding_key)
|
||||
- [Ejemplo de uso](#ejemplo-de-uso)
|
||||
- [Características del intercambio de temas](#características-del-intercambio-de-temas)
|
||||
- [Implementación del sistema de registro](#implementación-del-sistema-de-registro)
|
||||
- [RPC](#rpc)
|
||||
- [Interfaz del cliente](#interfaz-del-cliente)
|
||||
- [Cola de retorno (*Callback queue*)](#cola-de-retorno-callback-queue)
|
||||
- [ID de correlación (*Correlation id*)](#id-de-correlación-correlation-id)
|
||||
- [Resumen](#resumen)
|
||||
- [Poniéndolo todo junto](#poniéndolo-todo-junto)
|
||||
|
||||
|
||||
## Despliegue rabbitmq con docker
|
||||
|
||||
Para desplegar RabbitMQ rápidamente, puedes usar Docker. Ejecuta el siguiente comando para iniciar un contenedor con RabbitMQ y su consola de gestión:
|
||||
|
||||
```bash
|
||||
docker run -d --hostname my-rabbit --name some-rabbit -p 8080:15672 -p 5672:5672 rabbitmq:3-management
|
||||
```
|
||||
|
||||
Si prefieres usar docker-compose, utiliza el archivo [docker-compose.yaml](./docker-compose.yaml) con el siguiente comando:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Pruebas
|
||||
|
||||
Pruebas extraídas de los tutoriales de la [documentación oficial de RabbitMQ](https://www.rabbitmq.com/tutorials#queue-tutorials).
|
||||
|
||||
### Hello World
|
||||
|
||||
Lo más sencillo que hace algo.
|
||||
|
||||
Tenemos que diferenciar algunos conceptos:
|
||||
- **Producer**: es el que envía mensajes.
|
||||
- **Queue**: es donde se almacenan los mensajes.
|
||||
- **Consumer**: es el que recibe mensajes.
|
||||
|
||||

|
||||
|
||||
Vamos a programar un producer y un consumer en Python.
|
||||
|
||||
RabbitMQ habla múltiples protocolos. Este tutorial utiliza AMQP 0-9-1, que es un protocolo abierto de propósito general para mensajería.
|
||||
|
||||
Hay un gran número de clientes para RabbitMQ en muchos idiomas diferentes. En esta serie de tutoriales vamos a usar Pika 1.0.0, que es el cliente Python recomendado por el equipo de RabbitMQ. Para instalarlo puedes usar la herramienta de gestión de paquetes pip:
|
||||
|
||||
```bash
|
||||
pip install pika --upgrade
|
||||
```
|
||||
|
||||
Nuestro primer programa [send.py](./hello-world/send.py) será el producer que enviará un único mensaje a la cola. Este script también crea la cola `hola`.
|
||||
|
||||
El programa [receive.py](./hello-world/receive.py) será el consumer que recibirá mensajes de la cola y los imprimirá en pantalla.
|
||||
|
||||
Desde la instalación de rabbitmq puedes ver qué colas tiene RabbitMQ y cuántos mensajes hay en ellas con rabbitmqctl:
|
||||
|
||||
```bash
|
||||
sudo rabbitmqctl list_queues
|
||||
```
|
||||
|
||||
Antes tendrás que entrar en el contenedor de rabbitmq:
|
||||
|
||||
```bash
|
||||
docker exec -it rabbitmq-server bash
|
||||
```
|
||||
|
||||
Ahora, para probarlo, ejecuta el producer y el consumer en dos terminales diferentes:
|
||||
|
||||
```bash
|
||||
cd hello-world
|
||||
|
||||
python send.py
|
||||
|
||||
python receive.py
|
||||
```
|
||||
|
||||
### Work Queues
|
||||
|
||||
Reparto de tareas entre los trabajadores (el modelo de consumidores competidores).
|
||||
|
||||

|
||||
|
||||
|
||||
Antes hemos enviado un mensaje que contenía `¡Hola Mundo!`. Ahora enviaremos cadenas que representan tareas complejas. No tenemos una tarea del mundo real, como imágenes para ser redimensionadas o archivos pdf para ser renderizados, así que vamos a fingir que estamos ocupados usando la función `time.sleep()`. Tomaremos el número de puntos de la cadena como su complejidad; cada punto representará un segundo de «trabajo». Por ejemplo, una tarea falsa descrita por Hola... tardará tres segundos.
|
||||
|
||||
Vamos a modificar el anterior send.py para permitir el envío de mensajes arbitrarios desde la línea de comando. Le llamaremos [new_task.py](./02work-queues/new_task.py).
|
||||
|
||||
También modificaremos receive.py para simular un segundo trabajao por cada punto en el cuerpo del mensaje. Como sacará mensajes de la cola y realizará la tarea le llamaremos [worker.py](./02work-queues/worker.py).
|
||||
|
||||
|
||||
Ahora, si ejecutamos dos veces o más el script worker.py, veremos cómo se reparten las tareas entre los dos consumidores.
|
||||
|
||||
En dos terminales distintas:
|
||||
```bash
|
||||
cd 02work-queues
|
||||
|
||||
python worker.py
|
||||
```
|
||||
|
||||
Y en la tercera terminal enviaremos trabajos:
|
||||
```bash
|
||||
python new_task.py Primer mensaje.
|
||||
python new_task.py Segundo mensaje..
|
||||
python new_task.py Tercer mensaje...
|
||||
python new_task.py Cuarto mensaje....
|
||||
python new_task.py Quinto mensaje.....
|
||||
```
|
||||
|
||||
Por defecto, RabbitMQ enviará cada mensaje al siguiente consumidor, en secuencia. Por término medio, cada consumidor recibirá el mismo número de mensajes. Esta forma de distribuir mensajes se llama round-robin.
|
||||
|
||||
Para asegurarse de que un mensaje nunca se pierde, RabbitMQ soporta acuses de recibo de mensajes. Un ack(nowledgement) es enviado de vuelta por el consumidor para decirle a RabbitMQ que un mensaje en particular ha sido recibido, procesado y que RabbitMQ es libre de borrarlo.
|
||||
|
||||
> Apunte: `ack` es una abreviatura de acknowledgement (reconocimiento). En el caso de que un consumidor muera (su conexión se cierre, por ejemplo) sin enviar un ack, RabbitMQ entenderá que no ha procesado el mensaje y lo reenviará a otro consumidor. Si hay otros consumidores conectados a la cola, se les enviará el mensaje.
|
||||
|
||||
**Acuse de recibo olvidado**
|
||||
|
||||
Es un error común olvidar el basic_ack. Los mensajes se volverán a entregar cuando tu cliente salga (lo que puede parecer una redistribución aleatoria), pero RabbitMQ consumirá cada vez más memoria ya que no será capaz de liberar ningún mensaje no empaquetado.
|
||||
|
||||
Para depurar este tipo de errores puedes usar rabbitmqctl para imprimir el campo messages_unacknowledged:
|
||||
|
||||
```bash
|
||||
sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
|
||||
```
|
||||
|
||||
|
||||
### Publish/Subscribe
|
||||
|
||||
A diferencia de las colas de trabajo, donde cada tarea se entrega a un solo trabajador, este tutorial demuestra el patrón de publicación/suscripción, que entrega mensajes a múltiples consumidores.
|
||||
|
||||

|
||||
|
||||
El ejemplo es un sistema de registro con dos programas: uno que emite mensajes de registro y otro que los recibe y los imprime.
|
||||
|
||||
Cada instancia del programa receptor recibe todos los mensajes, permitiendo que los registros se dirijan al disco o se visualicen en pantalla.
|
||||
|
||||
**Enfoque:**
|
||||
|
||||
El ejemplo es un sistema de registro con dos programas: uno que emite mensajes de registro y otro que los recibe y los imprime.
|
||||
|
||||
Cada instancia del programa receptor recibe todos los mensajes, permitiendo que los registros se dirijan al disco o se visualicen en pantalla.
|
||||
|
||||
**Exchange:**
|
||||
En RabbitMQ, los productores envían mensajes a un intercambio, no directamente a una cola.
|
||||
|
||||
Un intercambio enruta los mensajes a las colas según las reglas definidas por su tipo.
|
||||
|
||||
Los tipos de intercambios incluyen directo, tópico, cabeceras y fanout. El tutorial se centra en fanout, que transmite mensajes a todas las colas conocidas.
|
||||
|
||||
Ejemplo de declaración de un intercambio fanout:
|
||||
|
||||
```python
|
||||
channel.exchange_declare(exchange='logs', exchange_type='fanout')
|
||||
```
|
||||
|
||||
**Colas Temporales:**
|
||||
Las colas temporales se crean con nombres generados aleatoriamente, y se eliminan automáticamente cuando se cierra la conexión del consumidor.
|
||||
Ejemplo de declaración de una cola temporal:
|
||||
|
||||
```python
|
||||
result = channel.queue_declare(queue='', exclusive=True)
|
||||
```
|
||||
**Código de Ejemplo:**
|
||||
|
||||
- [emit_log.py](./03_publish_subcribe/emit_log.py) para enviar mensajes de log.
|
||||
- [receive_logs.py](./03_publish_subcribe/receive_logs.py) para recibir mensajes de log.
|
||||
|
||||
|
||||
### Routing
|
||||
|
||||
En el tutorial anterior, creamos un sistema de registro simple que enviaba mensajes de log a múltiples receptores. En este tutorial, añadiremos la capacidad de suscribirse solo a un subconjunto de mensajes, permitiendo, por ejemplo, que solo los mensajes de error críticos se registren en un archivo, mientras que todos los mensajes de log se imprimen en la consola.
|
||||
|
||||

|
||||
|
||||
|
||||
#### Enlaces
|
||||
|
||||
En ejemplos anteriores, ya creamos enlaces entre intercambios (exchanges) y colas (queues). Un enlace determina qué colas están interesadas en los mensajes de un intercambio. Los enlaces pueden incluir una clave de enrutamiento (routing key) que especifica qué mensajes de un intercambio deben ser enviados a una cola.
|
||||
|
||||
```python
|
||||
channel.queue_bind(exchange=exchange_name,
|
||||
queue=queue_name,
|
||||
routing_key='black')
|
||||
```
|
||||
|
||||
La clave de enlace depende del tipo de intercambio. En un intercambio de tipo `fanout`, esta clave es ignorada.
|
||||
|
||||
#### Intercambio Directo
|
||||
|
||||
Anteriormente, usamos un intercambio de tipo `fanout` que transmitía todos los mensajes a todos los consumidores sin distinción. Ahora utilizaremos un intercambio `direct` que permite filtrar mensajes basándose en su severidad. Así, los mensajes serán enviados solo a las colas que coincidan exactamente con la clave de enrutamiento del mensaje.
|
||||
|
||||
Por ejemplo, si un intercambio tiene dos colas con claves de enlace `orange` y `black`, un mensaje con clave de enrutamiento `orange` solo irá a la cola correspondiente a `orange`.
|
||||
|
||||
#### Múltiples Enlaces
|
||||
|
||||
Es posible vincular varias colas con la misma clave de enlace. En este caso, el intercambio `direct` actúa como un `fanout`, enviando el mensaje a todas las colas que tengan una clave de enlace coincidente.
|
||||
|
||||
#### Emisión de Logs
|
||||
|
||||
Usaremos este modelo para nuestro sistema de logs. En lugar de `fanout`, enviaremos mensajes a un intercambio `direct`, usando la severidad del log como clave de enrutamiento.
|
||||
|
||||
Primero, debemos declarar un intercambio:
|
||||
|
||||
```python
|
||||
channel.exchange_declare(exchange='direct_logs',
|
||||
exchange_type='direct')
|
||||
```
|
||||
|
||||
Y luego podemos enviar un mensaje:
|
||||
|
||||
```python
|
||||
channel.basic_publish(exchange='direct_logs',
|
||||
routing_key=severity,
|
||||
body=message)
|
||||
```
|
||||
|
||||
Las severidades pueden ser `'info'`, `'warning'` o `'error'`.
|
||||
|
||||
#### Suscripción
|
||||
|
||||
Para recibir mensajes, crearemos un enlace para cada severidad de interés.
|
||||
|
||||
```python
|
||||
result = channel.queue_declare(queue='', exclusive=True)
|
||||
queue_name = result.method.queue
|
||||
|
||||
for severity in severities:
|
||||
channel.queue_bind(exchange='direct_logs',
|
||||
queue=queue_name,
|
||||
routing_key=severity)
|
||||
```
|
||||
|
||||
#### Código de Ejemplo
|
||||
|
||||
- **[emit_log_direct.py](./04_routing/emit_log_direct.py)**: Script para emitir mensajes de log.
|
||||
- **[receive_logs_direct.py](./04_routing/receive_logs_direct.py)**: Script para recibir mensajes de log.
|
||||
|
||||
|
||||
#### Ejemplos de Uso
|
||||
|
||||
- Para guardar solo los mensajes de `'warning'` y `'error'` en un archivo:
|
||||
```bash
|
||||
python receive_logs_direct.py warning error > logs_from_rabbit.log
|
||||
```
|
||||
|
||||
- Para ver todos los mensajes de log en pantalla:
|
||||
```bash
|
||||
python receive_logs_direct.py info warning error
|
||||
```
|
||||
|
||||
- Para emitir un mensaje de error:
|
||||
```bash
|
||||
python emit_log_direct.py error "Run. Run. Or it will explode."
|
||||
```
|
||||
|
||||
|
||||
### Topics (Próximamente)
|
||||
|
||||
En el tutorial anterior, mejoramos nuestro sistema de registro utilizando un intercambio de tipo `direct` para recibir registros selectivamente, basado en criterios como la severidad del mensaje. Sin embargo, para mayor flexibilidad, podemos usar un intercambio de tipo `topic`, que permite el enrutamiento de mensajes basado en múltiples criterios.
|
||||
|
||||

|
||||
|
||||
#### ¿Qué es un intercambio de temas?
|
||||
|
||||
- **`routing_key`**: En un intercambio de tipo `topic`, los mensajes tienen una clave de enrutamiento (`routing_key`) que es una lista de palabras separadas por puntos. Ejemplos: `"quick.orange.rabbit"`, `"lazy.brown.fox"`.
|
||||
- **`binding_key`**: Las claves de enlace (`binding_key`) también tienen el mismo formato y determinan qué mensajes recibe cada cola.
|
||||
|
||||
#### Casos especiales de `binding_key`
|
||||
|
||||
- **`*` (asterisco)**: Sustituye exactamente una palabra.
|
||||
- **`#` (almohadilla)**: Sustituye cero o más palabras.
|
||||
|
||||
#### Ejemplo de uso
|
||||
|
||||
Considera el siguiente escenario con dos colas (Q1 y Q2) y estas claves de enlace:
|
||||
|
||||
- Q1: `*.orange.*` (recibe todos los mensajes sobre animales naranjas)
|
||||
- Q2: `*.*.rabbit` y `lazy.#` (recibe todos los mensajes sobre conejos y animales perezosos)
|
||||
|
||||
Ejemplos de mensajes:
|
||||
|
||||
- `"quick.orange.rabbit"`: Entregado a Q1 y Q2.
|
||||
- `"lazy.orange.elephant"`: Entregado a Q1 y Q2.
|
||||
- `"quick.orange.fox"`: Solo entregado a Q1.
|
||||
- `"lazy.brown.fox"`: Solo entregado a Q2.
|
||||
|
||||
Mensajes con una o cuatro palabras, como `"orange"` o `"quick.orange.new.rabbit"`, no coinciden con ningún enlace y se pierden.
|
||||
|
||||
#### Características del intercambio de temas
|
||||
|
||||
- Puede comportarse como un intercambio `fanout` si se usa `#` como `binding_key` (recibe todos los mensajes).
|
||||
- Se comporta como un intercambio `direct` si no se utilizan `*` o `#` en las claves de enlace.
|
||||
|
||||
#### Implementación del sistema de registro
|
||||
|
||||
Usaremos un intercambio de temas para enrutar registros usando `routing_key` con el formato `<facilidad>.<severidad>`. El código para emitir y recibir registros es similar al de tutoriales anteriores.
|
||||
|
||||
**Ejemplos de comandos:**
|
||||
|
||||
- Recibir todos los registros: `python receive_logs_topic.py "#"`
|
||||
- Recibir registros de "kern": `python receive_logs_topic.py "kern.*"`
|
||||
- Recibir solo registros "critical": `python receive_logs_topic.py "*.critical"`
|
||||
- Emitir un registro crítico de "kern": `python emit_log_topic.py "kern.critical" "A critical kernel error"`
|
||||
|
||||
El código es casi el mismo que en el tutorial anterior.
|
||||
|
||||
- **[emit_log_topic.py](./05_topics/emit_log_topic.py)**
|
||||
- **[receive_logs_topic.py](./05_topics/receive_logs_topic)**
|
||||
|
||||
|
||||
### RPC
|
||||
|
||||
En el segundo tutorial aprendimos a usar *Work Queues* para distribuir tareas que consumen tiempo entre múltiples trabajadores.
|
||||
|
||||
Pero, ¿qué pasa si necesitamos ejecutar una función en una computadora remota y esperar el resultado? Eso es una historia diferente. Este patrón es comúnmente conocido como *Remote Procedure Call* o RPC.
|
||||
|
||||

|
||||
|
||||
En este tutorial vamos a usar RabbitMQ para construir un sistema RPC: un cliente y un servidor RPC escalable. Como no tenemos tareas que consuman tiempo que valga la pena distribuir, vamos a crear un servicio RPC ficticio que devuelva números de Fibonacci.
|
||||
|
||||
|
||||
#### Interfaz del cliente
|
||||
|
||||
Para ilustrar cómo se podría usar un servicio RPC, vamos a crear una clase de cliente simple. Va a exponer un método llamado `call` que envía una solicitud RPC y se bloquea hasta que se recibe la respuesta:
|
||||
|
||||
```python
|
||||
fibonacci_rpc = FibonacciRpcClient()
|
||||
result = fibonacci_rpc.call(4)
|
||||
print(f"fib(4) is {result}")
|
||||
```
|
||||
|
||||
**Una nota sobre RPC**
|
||||
|
||||
Aunque RPC es un patrón bastante común en informática, a menudo se critica. Los problemas surgen cuando un programador no está al tanto de si una llamada a función es local o si es un RPC lento. Confusiones como esas resultan en un sistema impredecible y añaden una complejidad innecesaria a la depuración. En lugar de simplificar el software, el uso indebido de RPC puede resultar en un código enredado e inmantenible.
|
||||
|
||||
Teniendo esto en cuenta, considere los siguientes consejos:
|
||||
|
||||
- Asegúrese de que sea obvio qué llamada a función es local y cuál es remota.
|
||||
- Documente su sistema. Haga claras las dependencias entre componentes.
|
||||
- Maneje los casos de error. ¿Cómo debería reaccionar el cliente cuando el servidor RPC está caído por mucho tiempo?
|
||||
|
||||
En caso de duda, evite RPC. Si puede, debe usar un canal asincrónico: en lugar de un bloqueo estilo RPC, los resultados se envían asincrónicamente a la siguiente etapa de computación.
|
||||
|
||||
|
||||
#### Cola de retorno (*Callback queue*)
|
||||
|
||||
En general, hacer RPC sobre RabbitMQ es fácil. Un cliente envía un mensaje de solicitud y un servidor responde con un mensaje de respuesta. Para recibir una respuesta, el cliente necesita enviar una dirección de una cola de retorno (*callback queue*) con la solicitud. Vamos a intentarlo:
|
||||
|
||||
```python
|
||||
result = channel.queue_declare(queue='', exclusive=True)
|
||||
callback_queue = result.method.queue
|
||||
|
||||
channel.basic_publish(exchange='',
|
||||
routing_key='rpc_queue',
|
||||
properties=pika.BasicProperties(
|
||||
reply_to = callback_queue,
|
||||
),
|
||||
body=request)
|
||||
|
||||
# ... y algo de código para leer un mensaje de respuesta de la callback_queue ...
|
||||
```
|
||||
|
||||
**Propiedades del mensaje**
|
||||
|
||||
El protocolo AMQP 0-9-1 predefine un conjunto de 14 propiedades que acompañan un mensaje. La mayoría de las propiedades rara vez se utilizan, con la excepción de las siguientes:
|
||||
|
||||
- `delivery_mode`: Marca un mensaje como persistente (con un valor de 2) o transitorio (cualquier otro valor). Puede recordar esta propiedad del segundo tutorial.
|
||||
- `content_type`: Se utiliza para describir el tipo MIME de la codificación. Por ejemplo, para la codificación JSON a menudo utilizada, es una buena práctica establecer esta propiedad en: `application/json`.
|
||||
- `reply_to`: Comúnmente utilizado para nombrar una cola de retorno (*callback queue*).
|
||||
- `correlation_id`: Útil para correlacionar respuestas RPC con solicitudes.
|
||||
|
||||
|
||||
#### ID de correlación (*Correlation id*)
|
||||
|
||||
En el método presentado anteriormente, sugerimos crear una cola de retorno para cada solicitud RPC. Eso es bastante ineficiente, pero afortunadamente hay una mejor manera: crear una única cola de retorno por cliente.
|
||||
|
||||
Eso plantea un nuevo problema: al recibir una respuesta en esa cola, no está claro a qué solicitud pertenece la respuesta. Es entonces cuando se usa la propiedad `correlation_id`. Vamos a configurarla a un valor único para cada solicitud. Más tarde, cuando recibamos un mensaje en la cola de retorno, miraremos esta propiedad, y con base en eso podremos coincidir una respuesta con una solicitud. Si vemos un valor de `correlation_id` desconocido, podemos descartar el mensaje de manera segura: no pertenece a nuestras solicitudes.
|
||||
|
||||
Puede preguntar, ¿por qué deberíamos ignorar los mensajes desconocidos en la cola de retorno en lugar de fallar con un error? Se debe a la posibilidad de una condición de carrera en el lado del servidor. Aunque es poco probable, es posible que el servidor RPC muera justo después de enviarnos la respuesta, pero antes de enviar un mensaje de confirmación para la solicitud. Si eso sucede, el servidor RPC reiniciado procesará la solicitud nuevamente. Es por eso que, en el cliente, debemos manejar las respuestas duplicadas de manera prudente, y el RPC idealmente debería ser idempotente.
|
||||
|
||||
|
||||
#### Resumen
|
||||
|
||||
Nuestro RPC funcionará así:
|
||||
|
||||
- Cuando el cliente se inicia, crea una cola de retorno anónima exclusiva.
|
||||
- Para una solicitud RPC, el cliente envía un mensaje con dos propiedades: `reply_to`, que se establece en la cola de retorno, y `correlation_id`, que se establece en un valor único para cada solicitud.
|
||||
- La solicitud se envía a una cola llamada `rpc_queue`.
|
||||
- El trabajador RPC (también conocido como servidor) está esperando solicitudes en esa cola. Cuando aparece una solicitud, hace el trabajo y envía un mensaje con el resultado de vuelta al cliente, usando la cola del campo `reply_to`.
|
||||
- El cliente espera datos en la cola de retorno. Cuando aparece un mensaje, verifica la propiedad `correlation_id`. Si coincide con el valor de la solicitud, devuelve la respuesta a la aplicación.
|
||||
|
||||
|
||||
#### Poniéndolo todo junto
|
||||
|
||||
- [rpc_server.py](./06_rpc/rpc_client.py)
|
||||
|
||||
El código del servidor es bastante sencillo:
|
||||
|
||||
- Como de costumbre, comenzamos estableciendo la conexión y declarando la cola `rpc_queue`.
|
||||
- Declaramos nuestra función de Fibonacci. Asume solo una entrada de entero positivo válida. (No espere que funcione para números grandes, probablemente sea la implementación recursiva más lenta posible).
|
||||
- Declaramos un *callback* `on_request` para `basic_consume`, el núcleo del servidor RPC. Se ejecuta cuando se recibe la solicitud. Hace el trabajo y envía la respuesta de vuelta.
|
||||
- Podríamos querer ejecutar más de un proceso de servidor. Para distribuir la carga de manera equitativa entre varios servidores, necesitamos establecer el ajuste `prefetch_count`.
|
||||
|
||||
[rpc_client.py](-/06_rpc/rpc_client.py)
|
||||
|
||||
El código del cliente es un poco más complejo:
|
||||
|
||||
- Establecemos una conexión, un canal y declaramos una `callback_queue` exclusiva para las respuestas.
|
||||
- Nos suscribimos a la `callback_queue`, para que podamos recibir respuestas RPC.
|
||||
- El *callback* `on_response` que se ejecuta en cada respuesta está haciendo un trabajo muy simple: para cada mensaje de respuesta, verifica si el `correlation_id` es el que estamos buscando. Si es así, guarda la respuesta en `self.response` y sale del bucle de consumo.
|
||||
- A continuación, definimos nuestro método principal `call`: realiza la solicitud RPC real.
|
||||
- En el método `call`, generamos un número de `correlation_id` único y lo guardamos: la función de *callback* `on_response` usará este valor para capturar
|
||||
|
||||
la respuesta apropiada.
|
||||
- También en el método `call`, publicamos el mensaje de solicitud, con dos propiedades: `reply_to` y `correlation_id`.
|
||||
- Al final, esperamos hasta que llegue la respuesta adecuada y devolvemos la respuesta al usuario.
|
||||
|
||||
Nuestro servicio RPC ya está listo. Podemos iniciar el servidor:
|
||||
|
||||
```bash
|
||||
python rpc_server.py
|
||||
```
|
||||
|
||||
Para solicitar un número de Fibonacci, ejecute el cliente con el número como argumento:
|
||||
|
||||
```bash
|
||||
python rpc_client.py <numero>
|
||||
```
|
||||
|
||||
El diseño presentado no es la única implementación posible de un servicio RPC, pero tiene algunas ventajas importantes:
|
||||
|
||||
- Si el servidor RPC es demasiado lento, puede escalar simplemente ejecutando otro. Intente ejecutar un segundo `rpc_server.py` en una nueva consola.
|
||||
- En el lado del cliente, el RPC requiere enviar y recibir solo un mensaje. No se requieren llamadas sincrónicas como `queue_declare`. Como resultado, el cliente RPC necesita solo un viaje de ida y vuelta por la red para una única solicitud RPC.
|
||||
|
||||
Nuestro código sigue siendo bastante simplista y no intenta resolver problemas más complejos (pero importantes), como:
|
||||
|
||||
- ¿Cómo debería reaccionar el cliente si no hay servidores en funcionamiento?
|
||||
- ¿Debería un cliente tener algún tipo de tiempo de espera para el RPC?
|
||||
- Si el servidor funciona mal y genera una excepción, ¿debería enviarse al cliente?
|
||||
- Proteger contra mensajes entrantes no válidos (por ejemplo, verificando límites) antes de procesar.
|
||||
|
||||
---
|
||||
|
||||
32
catch-all/05_infra_test/02_rabbitmq/docker-compose.yaml
Normal file
32
catch-all/05_infra_test/02_rabbitmq/docker-compose.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management-alpine
|
||||
container_name: 'rabbitmq-server'
|
||||
hostname: rabbitmq
|
||||
ports:
|
||||
- 5672:5672
|
||||
- 15672:15672
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ./rabbitmq/data/:/var/lib/rabbitmq/
|
||||
- ./rabbitmq/log/:/var/log/rabbitmq
|
||||
# environment:
|
||||
# RABBITMQ_DEFAULT_USER: invent
|
||||
# RABBITMQ_DEFAULT_PASS: 123456
|
||||
# RABBITMQ_ERLANG_COOKIE: 'randomcookievalue'
|
||||
networks:
|
||||
- rabbitmq_go_net
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
description: "RabbitMQ Server in container docker"
|
||||
maintainer: "manuelver"
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmqctl", "status"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
rabbitmq_go_net:
|
||||
driver: bridge
|
||||
318
catch-all/05_infra_test/03_kafka/README.md
Normal file
318
catch-all/05_infra_test/03_kafka/README.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Empezando con Apache Kafka en Python
|
||||
|
||||
En este artículo, voy a hablar sobre Apache Kafka y cómo los programadores de Python pueden usarlo para construir sistemas distribuidos.
|
||||
|
||||
|
||||
## ¿Qué es Apache Kafka?
|
||||
|
||||
Apache Kafka es una plataforma de streaming de código abierto que fue inicialmente desarrollada por LinkedIn. Más tarde fue entregada a la fundación Apache y se convirtió en código abierto en 2011.
|
||||
|
||||
Según [Wikipedia](https://en.wikipedia.org/wiki/Apache_Kafka):
|
||||
|
||||
__Apache Kafka es una plataforma de software de procesamiento de flujos de código abierto desarrollada por la Fundación Apache, escrita en Scala y Java. El proyecto tiene como objetivo proporcionar una plataforma unificada, de alto rendimiento y baja latencia para manejar flujos de datos en tiempo real. Su capa de almacenamiento es esencialmente una "cola de mensajes pub/sub masivamente escalable arquitectada como un registro de transacciones distribuidas", lo que la hace altamente valiosa para las infraestructuras empresariales para procesar datos de streaming. Además, Kafka se conecta a sistemas externos (para importación/exportación de datos) a través de Kafka Connect y proporciona Kafka Streams, una biblioteca de procesamiento de flujos en Java.__
|
||||
|
||||

|
||||
|
||||
Piense en él como un gran registro de commits donde los datos se almacenan en secuencia a medida que ocurren. Los usuarios de este registro pueden acceder y usarlo según sus necesidades.
|
||||
|
||||
|
||||
## Casos de uso de Kafka
|
||||
|
||||
Los usos de Kafka son múltiples. Aquí hay algunos casos de uso que podrían ayudarte a comprender su aplicación:
|
||||
|
||||
- **Monitoreo de Actividad**: Kafka puede ser utilizado para el monitoreo de actividad. La actividad puede pertenecer a un sitio web o a sensores y dispositivos físicos. Los productores pueden publicar datos en bruto de las fuentes de datos que luego pueden usarse para encontrar tendencias y patrones.
|
||||
- **Mensajería**: Kafka puede ser utilizado como un intermediario de mensajes entre servicios. Si estás implementando una arquitectura de microservicios, puedes tener un microservicio como productor y otro como consumidor. Por ejemplo, tienes un microservicio responsable de crear nuevas cuentas y otro para enviar correos electrónicos a los usuarios sobre la creación de la cuenta.
|
||||
- **Agregación de Logs**: Puedes usar Kafka para recopilar logs de diferentes sistemas y almacenarlos en un sistema centralizado para un procesamiento posterior.
|
||||
- **ETL**: Kafka tiene una característica de streaming casi en tiempo real, por lo que puedes crear un ETL basado en tus necesidades.
|
||||
- **Base de Datos**: Basado en lo que mencioné antes, podrías decir que Kafka también actúa como una base de datos. No una base de datos típica que tiene una característica de consulta de datos según sea necesario, lo que quise decir es que puedes mantener datos en Kafka todo el tiempo que quieras sin consumirlos.
|
||||
|
||||
|
||||
## Conceptos de Kafka
|
||||
|
||||

|
||||
|
||||
|
||||
### Topics
|
||||
|
||||
Cada mensaje que se introduce en el sistema debe ser parte de algún topic. El topic no es más que un flujo de registros. Los mensajes se almacenan en formato clave-valor. A cada mensaje se le asigna una secuencia, llamada Offset. La salida de un mensaje podría ser una entrada de otro para un procesamiento posterior.
|
||||
|
||||
|
||||
### Producers
|
||||
|
||||
Los Producers son las aplicaciones responsables de publicar datos en el sistema Kafka. Publican datos en el topic de su elección.
|
||||
|
||||
|
||||
### Consumers
|
||||
|
||||
Los mensajes publicados en los topics son luego utilizados por las aplicaciones Consumers. Un consumer se suscribe al topic de su elección y consume datos.
|
||||
|
||||
|
||||
### Broker
|
||||
|
||||
Cada instancia de Kafka que es responsable del intercambio de mensajes se llama Broker. Kafka puede ser utilizado como una máquina independiente o como parte de un cluster.
|
||||
|
||||
Trataré de explicar todo con un ejemplo simple: hay un almacén de un restaurante donde se almacenan todos los ingredientes como arroz, verduras, etc. El restaurante sirve diferentes tipos de platos: chino, desi, italiano, etc. Los chefs de cada cocina pueden referirse al almacén, elegir lo que desean y preparar los platos. Existe la posibilidad de que lo que se haga con el material crudo pueda ser usado más tarde por todos los chefs de los diferentes departamentos, por ejemplo, una salsa secreta que se usa en TODO tipo de platos. Aquí, el almacén es un broker, los proveedores de bienes son los producers, los bienes y la salsa secreta hecha por los chefs son los topics, mientras que los chefs son los consumers. Mi analogía puede sonar divertida e inexacta, pero al menos te habría ayudado a entender todo :-)
|
||||
|
||||
|
||||
## Configuración y Ejecución
|
||||
|
||||
La forma más sencilla de instalar Kafka es descargar los binarios y ejecutarlo. Dado que está basado en lenguajes JVM como Scala y Java, debes asegurarte de estar usando Java 7 o superior.
|
||||
|
||||
Kafka está disponible en dos versiones diferentes: Una por la [fundación Apache](https://kafka.apache.org/downloads) y otra por [Confluent](https://www.confluent.io/about/) como un [paquete](https://www.confluent.io/download/). Para este tutorial, utilizaré la proporcionada por la fundación Apache. Por cierto, Confluent fue fundada por los [desarrolladores originales](https://www.confluent.io/about/) de Kafka.
|
||||
|
||||
|
||||
## Iniciando Zookeeper
|
||||
|
||||
Kafka depende de Zookeeper, para hacerlo funcionar primero tendremos que ejecutar Zookeeper.
|
||||
|
||||
```
|
||||
bin/zookeeper-server-start.sh config/zookeeper.properties
|
||||
```
|
||||
|
||||
mostrará muchos textos en la pantalla, si ves lo siguiente significa que está correctamente configurado.
|
||||
|
||||
```
|
||||
2018-06-10 06:36:15,023] INFO maxSessionTimeout set to -1 (org.apache.zookeeper.server.ZooKeeperServer)
|
||||
[2018-06-10 06:36:15,044] INFO binding to port 0.0.0.0/0.0.0.0:2181 (org.apache.zookeeper.server.NIOServerCnxnFactory)
|
||||
```
|
||||
|
||||
|
||||
## Iniciando el Servidor Kafka
|
||||
|
||||
A continuación, tenemos que iniciar el servidor broker de Kafka:
|
||||
|
||||
```
|
||||
bin/kafka-server-start.sh config/server.properties
|
||||
```
|
||||
|
||||
Y si ves el siguiente texto en la consola significa que está en funcionamiento.
|
||||
|
||||
```
|
||||
2018-06-10 06:38:44,477] INFO Kafka commitId : fdcf75ea326b8e07 (org.apache.kafka.common.utils.AppInfoParser)
|
||||
[2018-06-10 06:38:44,478] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)
|
||||
```
|
||||
|
||||
|
||||
## Crear Topics
|
||||
|
||||
Los mensajes se publican en topics. Usa este comando para crear un nuevo topic.
|
||||
|
||||
```
|
||||
➜ kafka_2.11-1.1.0 bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
|
||||
Created topic "test".
|
||||
```
|
||||
|
||||
También puedes listar todos los topics disponibles ejecutando el siguiente comando.
|
||||
|
||||
```
|
||||
➜ kafka_2.11-1.1.0 bin/kafka-topics.sh --list --zookeeper localhost:2181
|
||||
test
|
||||
```
|
||||
|
||||
Como ves, imprime, test.
|
||||
|
||||
|
||||
## Enviar Mensajes
|
||||
|
||||
A continuación, tenemos que enviar mensajes, los *producers* se utilizan para ese propósito. Vamos a iniciar un *producer*.
|
||||
|
||||
```
|
||||
➜ kafka_2.11-1.1.0 bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
|
||||
>Hello
|
||||
>World
|
||||
```
|
||||
|
||||
Inicias la interfaz del *producer* basada en consola que se ejecuta en el puerto 9092 por defecto. `--topic` te permite establecer el *topic* en el que se publicarán los mensajes. En nuestro caso, el *topic* es `test`.
|
||||
|
||||
Te muestra un prompt `>` y puedes ingresar lo que quieras.
|
||||
|
||||
Los mensajes se almacenan localmente en tu disco. Puedes conocer la ruta de almacenamiento verificando el valor de `log.dirs` en el archivo `config/server.properties`. Por defecto, están configurados en `/tmp/kafka-logs/`.
|
||||
|
||||
Si listamos esta carpeta, encontraremos una carpeta con el nombre `test-0`. Al listar su contenido encontrarás 3 archivos: `00000000000000000000.index 00000000000000000000.log 00000000000000000000.timeindex`.
|
||||
|
||||
Si abres `00000000000000000000.log` en un editor, muestra algo como:
|
||||
|
||||
```
|
||||
^@^@^@^@^@^@^@^@^@^@^@=^@^@^@^@^BÐØR^V^@^@^@^@^@^@^@^@^Acça<9a>o^@^@^Acça<9a>oÿÿÿÿÿÿÿÿÿÿÿÿÿÿ^@^@^@^A^V^@^@^@^A
|
||||
Hello^@^@^@^@^@^@^@^@^A^@^@^@=^@^@^@^@^BÉJ^B^@^@^@^@^@^@^@^@^Acça<9f>^?^@^@^Acça<9f>^?ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ^@^@^@^A^V^@^@^@^A
|
||||
World^@
|
||||
~
|
||||
```
|
||||
|
||||
Parece que los datos están codificados o delimitados por separadores, no estoy seguro. Si alguien conoce este formato, que me lo haga saber.
|
||||
|
||||
De todos modos, Kafka proporciona una utilidad que te permite examinar cada mensaje entrante.
|
||||
|
||||
```
|
||||
➜ kafka_2.11-1.1.0 bin/kafka-run-class.sh kafka.tools.DumpLogSegments --deep-iteration --print-data-log --files /tmp/kafka-logs/test-0/00000000000000000000.log
|
||||
Dumping /tmp/kafka-logs/test-0/00000000000000000000.log
|
||||
Starting offset: 0
|
||||
offset: 0 position: 0 CreateTime: 1528595323503 isvalid: true keysize: -1 valuesize: 5 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] payload: Hello
|
||||
offset: 1 position: 73 CreateTime: 1528595324799 isvalid: true keysize: -1 valuesize: 5 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] payload: World
|
||||
```
|
||||
|
||||
Puedes ver el mensaje con otros detalles como `offset`, `position` y `CreateTime`, etc.
|
||||
|
||||
|
||||
## Consumir Mensajes
|
||||
|
||||
Los mensajes que se almacenan también deben ser consumidos. Vamos a iniciar un *consumer* basado en consola.
|
||||
|
||||
```
|
||||
➜ kafka_2.11-1.1.0 bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
|
||||
```
|
||||
|
||||
Si lo ejecutas, volcará todos los mensajes desde el principio hasta ahora. Si solo estás interesado en consumir los mensajes después de ejecutar el *consumer*, entonces puedes simplemente omitir el interruptor `--from-beginning` y ejecutarlo. La razón por la cual no muestra los mensajes antiguos es porque el *offset* se actualiza una vez que el *consumer* envía un ACK al *broker* de Kafka sobre el procesamiento de mensajes. Puedes ver el flujo de trabajo a continuación.
|
||||
|
||||

|
||||
|
||||
|
||||
## Acceder a Kafka en Python
|
||||
|
||||
Hay múltiples librerías de Python disponibles para su uso:
|
||||
|
||||
- **Kafka-Python** — Una librería de código abierto basada en la comunidad.
|
||||
- **PyKafka** — Esta librería es mantenida por Parsly y se dice que es una API Pythónica. A diferencia de Kafka-Python, no puedes crear *topics* dinámicos.
|
||||
- **Confluent Python Kafka**:— Es ofrecida por Confluent como un contenedor delgado alrededor de **librdkafka**, por lo tanto, su rendimiento es mejor que el de las dos anteriores.
|
||||
|
||||
Para este artículo, usaremos la librería de código abierto Kafka-Python.
|
||||
|
||||
|
||||
## Sistema de Alerta de Recetas en Kafka
|
||||
|
||||
En el último artículo sobre Elasticsearch, extraje datos de Allrecipes. En este artículo, voy a usar el mismo scraper como fuente de datos. El sistema que vamos a construir es un sistema de alertas que enviará notificaciones sobre las recetas si cumplen con cierto umbral de calorías. Habrá dos *topics*:
|
||||
|
||||
- **raw_recipes**:— Almacenará el HTML en bruto de cada receta. La idea es usar este *topic* como la fuente principal de nuestros datos que luego pueden ser procesados y transformados según sea necesario.
|
||||
- **parsed_recipes**:— Como su nombre indica, este será el dato analizado de cada receta en formato JSON.
|
||||
|
||||
La longitud del nombre de un *topic* en Kafka no debe exceder los 249 caracteres.
|
||||
|
||||
Un flujo de trabajo típico se verá así:
|
||||
|
||||

|
||||
|
||||
|
||||
Instala `kafka-python` mediante `pip`.
|
||||
|
||||
```
|
||||
pip install kafka-python
|
||||
```
|
||||
|
||||
|
||||
## Productor de Recetas en Bruto
|
||||
|
||||
El primer programa que vamos a escribir es el *producer*. Accederá a Allrecipes.com y obtendrá el HTML en bruto y lo almacenará en el *topic* `raw_recipes`. Archivo [producer-raw-recipes.py](./producer-raw-recipes/producer-raw-recipies.py).
|
||||
|
||||
Este fragmento de código extraerá el marcado de cada receta y lo devolverá en formato `list`.
|
||||
|
||||
A continuación, debemos crear un objeto *producer*. Antes de proceder, haremos cambios en el archivo `config/server.properties`. Debemos establecer `advertised.listeners` en `PLAINTEXT://localhost:9092`, de lo contrario, podrías experimentar el siguiente error:
|
||||
|
||||
```
|
||||
Error encountered when producing to broker b'adnans-mbp':9092. Retrying.
|
||||
```
|
||||
|
||||
Ahora añadiremos dos métodos: `connect_kafka_producer()` que te dará una instancia del *producer* de Kafka y `publish_message()` que solo almacenará el HTML en bruto de recetas individuales.
|
||||
|
||||
El __main__ se verá así.
|
||||
|
||||
Si funciona bien, mostrará la siguiente salida:
|
||||
|
||||
```
|
||||
/anaconda3/anaconda/bin/python /Development/DataScience/Kafka/kafka-recipie-alert/producer-raw-recipies.py
|
||||
Accessing list
|
||||
Processing..https://www.allrecipes.com/recipe/20762/california-coleslaw/
|
||||
Processing..https://www.allrecipes.com/recipe/8584/holiday-chicken-salad/
|
||||
Processing..https://www.allrecipes.com/recipe/80867/cran-broccoli-salad/
|
||||
Message published successfully.
|
||||
Message published successfully.
|
||||
Message published successfully.Process finished with exit code 0
|
||||
```
|
||||
|
||||
Estoy usando una herramienta GUI, llamada Kafka Tool, para navegar por los mensajes publicados recientemente. Está disponible para OSX, Windows y Linux.
|
||||
|
||||

|
||||
|
||||
|
||||
## Analizador de Recetas
|
||||
|
||||
Archivo [producer-consumer-parse-recipes.py](./producer-consumer-parse-recipes/producer-consumer-parse-recipes.py).
|
||||
|
||||
El siguiente script que vamos a escribir servirá como *consumer* y *producer*. Primero consumirá datos del *topic* `raw_recipes`, analizará y transformará los datos en JSON, y luego los publicará en el *topic* `parsed_recipes`. A continuación se muestra el código que obtendrá datos HTML del *topic* `raw_recipes`, los analizará y luego los alimentará al *topic* `parsed_recipes`.
|
||||
|
||||
`KafkaConsumer` acepta algunos parámetros además del nombre del *topic* y la dirección del host. Al proporcionar `auto_offset_reset='earliest'` le estás diciendo a Kafka que devuelva mensajes desde el principio. El parámetro `consumer_timeout_ms` ayuda al *consumer* a desconectarse después de cierto período de tiempo. Una vez desconectado, puedes cerrar el flujo del *consumer* llamando a `consumer.close()`.
|
||||
|
||||
Después de esto, estoy utilizando las mismas rutinas para conectar *producers* y publicar datos analizados en el nuevo *topic*. El navegador KafkaTool da buenas noticias sobre los mensajes almacenados recientemente.
|
||||
|
||||

|
||||
|
||||
Hasta ahora, todo bien. Almacenamos las recetas en formato bruto y JSON para uso futuro. A continuación, tenemos que escribir un *consumer* que se conecte con el *topic* `parsed_recipes` y genere una alerta si se cumple cierto criterio de `calories`.
|
||||
|
||||
El JSON se decodifica y luego se verifica la cantidad de calorías, se emite una notificación una vez que se cumple el criterio.
|
||||
|
||||
|
||||
## Ya lo hemos probado en local, ¡ahora a dockerizarlo!
|
||||
|
||||
Ahora que hemos probado el sistema localmente, es hora de dockerizarlo para facilitar su despliegue y escalabilidad. Vamos a crear imágenes Docker para cada uno de los scripts de Python y configurar un entorno de Docker Compose para orquestar todo.
|
||||
|
||||
Esctrutura de directorios:
|
||||
|
||||
```
|
||||
.
|
||||
├── docker-compose.yml
|
||||
├── producer-raw-recipes/
|
||||
│ ├── Dockerfile
|
||||
│ └── producer-raw-recipes.py
|
||||
├── producer-consumer-parse-recipes/
|
||||
│ ├── Dockerfile
|
||||
│ └── producer_consumer_parse_recipes.py
|
||||
└── consumer-notification/
|
||||
├── Dockerfile
|
||||
└── consumer-notification.py
|
||||
```
|
||||
|
||||
Ficheros docker:
|
||||
- [docker-compose.yaml](./docker-compose.yml)
|
||||
- [Dockerfile producer-raw-recipes](./producer-raw-recipes/Dockerfile)
|
||||
- [Dockerfile producer-consumer-parse-recipes](./producer-consumer-parse-recipes/Dockerfile)
|
||||
- [Dockerfile consumer-notification](./consumer-notification/Dockerfile)
|
||||
|
||||
Los ficheros Dockerfile construye las imágenes Docker para cada uno de los scripts de Python.
|
||||
|
||||
El archivo Docker Compose configura los siguientes servicios y las aplicaciones que se ejecutarán:
|
||||
|
||||
- **Zookeeper**: Para coordinar el cluster de Kafka.
|
||||
- **Kafka**: El broker de Kafka.
|
||||
- **Kafdrop**: Una interfaz web para Kafka.
|
||||
- **producer-raw-recipes**: El productor que envía recetas en bruto.
|
||||
- **producer-consumer-parse-recipes**: El productor-consumidor que analiza las recetas y las envía a un nuevo topic.
|
||||
- **consumer-notification**: El consumidor que emite alertas sobre recetas con alto contenido calórico.
|
||||
|
||||
El docker-compose es muy completo con healthchecks, dependencias, volumenes, etc.
|
||||
|
||||
### Construir y Ejecutar los Contenedores
|
||||
|
||||
Para construir y ejecutar los contenedores, usa el siguiente comando en el directorio raíz del proyecto:
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Este comando construye las imágenes Docker y levanta los contenedores definidos en `docker-compose.yml`.
|
||||
|
||||
|
||||
### Verificación
|
||||
|
||||
Para verificar que todo está funcionando correctamente:
|
||||
|
||||
1. Asegúrate de que todos los contenedores están en ejecución usando `docker ps`.
|
||||
2. Revisa los logs de cada contenedor con `docker logs <container_name>` para asegurar que no haya errores. Con lazydocker puedes ver los logs de todos los contenedores rápidamente.
|
||||
3. Puedes usar herramientas GUI como Kafka Tool o `kafka-console-consumer.sh` y `kafka-console-producer.sh` para interactuar con los topics y los mensajes.
|
||||
4. Panel de control de Kafdrop en `http://localhost:9000`.
|
||||
|
||||
|
||||
## Conclusión
|
||||
|
||||
Kafka es un sistema de mensajería de publicación-suscripción escalable y tolerante a fallos que te permite construir aplicaciones distribuidas. Debido a su [alto rendimiento y eficiencia](http://searene.me/2017/07/09/Why-is-Kafka-so-fast/), se está volviendo popular entre las empresas que producen grandes cantidades de datos desde diversas fuentes externas y desean proporcionar resultados en tiempo real a partir de ellos. Solo he cubierto lo esencial. Explora los documentos y las implementaciones existentes, y te ayudará a entender cómo podría ser la mejor opción para tu próximo sistema.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Usa una imagen base de Python
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Configura el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia el archivo de requisitos al contenedor
|
||||
COPY requirements.txt /app/
|
||||
|
||||
# Instala las dependencias
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el script de Python al contenedor
|
||||
COPY consumer-notification.py /app/
|
||||
|
||||
# Comando por defecto para ejecutar el script
|
||||
CMD ["python", "consumer-notification.py"]
|
||||
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
import logging
|
||||
from time import sleep
|
||||
from kafka import KafkaConsumer
|
||||
|
||||
# Configuración del registro
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# Nombre del tema de Kafka del que se consumen los mensajes
|
||||
parsed_topic_name = 'parsed_recipes'
|
||||
|
||||
# Umbral de calorías para la notificación
|
||||
calories_threshold = 200
|
||||
|
||||
# Crear un consumidor de Kafka
|
||||
consumer = KafkaConsumer(
|
||||
parsed_topic_name,
|
||||
auto_offset_reset='earliest', # Inicia desde el 1er mensaje si es nuevo
|
||||
bootstrap_servers=['kafka:9092'], # Servidor Kafka
|
||||
api_version=(0, 10), # Versión de la API de Kafka
|
||||
# Tiempo máx. de espera mensajes (milisegundos)
|
||||
consumer_timeout_ms=2000
|
||||
)
|
||||
|
||||
logging.info('[+] Iniciando el consumidor de notificaciones...')
|
||||
|
||||
try:
|
||||
|
||||
for msg in consumer:
|
||||
|
||||
# Decodificar el mensaje de JSON
|
||||
record = json.loads(msg.value)
|
||||
|
||||
# Obtener el valor de calorías
|
||||
calories = int(record.get('calories', 0))
|
||||
title = record.get('title', 'Sin título') # Obtener el título
|
||||
|
||||
# Verificar si las calorías exceden el umbral
|
||||
if calories > calories_threshold:
|
||||
logging.warning(
|
||||
f'[!] Alerta: {title} tiene {calories} calorías')
|
||||
|
||||
# Esperar 5 segundos antes de procesar el siguiente mensaje
|
||||
sleep(5)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error(
|
||||
'[!] Error en el consumidor de notificaciones', exc_info=True)
|
||||
|
||||
finally:
|
||||
|
||||
if consumer is not None:
|
||||
consumer.close() # Cerrar el consumidor al finalizar
|
||||
logging.info('[i] Consumidor cerrado.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
kafka-python
|
||||
153
catch-all/05_infra_test/03_kafka/docker-compose.yaml
Normal file
153
catch-all/05_infra_test/03_kafka/docker-compose.yaml
Normal file
@@ -0,0 +1,153 @@
|
||||
services:
|
||||
zookeeper:
|
||||
image: bitnami/zookeeper:latest
|
||||
container_name: zookeeper
|
||||
ports:
|
||||
- "2181:2181"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- "zookeeper_data:/bitnami"
|
||||
environment:
|
||||
ALLOW_ANONYMOUS_LOGIN: "yes"
|
||||
networks:
|
||||
- kafka-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "nc -z localhost 2181"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
|
||||
kafka:
|
||||
image: bitnami/kafka:latest
|
||||
container_name: kafka
|
||||
ports:
|
||||
- "9092:9092"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- "kafka_data:/bitnami"
|
||||
environment:
|
||||
KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9092,OUTSIDE://localhost:9093
|
||||
KAFKA_LISTENERS: INSIDE://0.0.0.0:9092,OUTSIDE://0.0.0.0:9093
|
||||
KAFKA_LISTENER_NAME: INSIDE
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL: PLAINTEXT
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
networks:
|
||||
- kafka-network
|
||||
depends_on:
|
||||
zookeeper:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "kafka-broker-api-versions.sh --bootstrap-server localhost:9092"]
|
||||
interval: 10s
|
||||
retries: 10
|
||||
start_period: 60s
|
||||
timeout: 10s
|
||||
|
||||
kafdrop:
|
||||
image: obsidiandynamics/kafdrop:latest
|
||||
container_name: kafdrop
|
||||
ports:
|
||||
- "9000:9000"
|
||||
environment:
|
||||
KAFKA_BROKERCONNECT: kafka:9092
|
||||
networks:
|
||||
- kafka-network
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
depends_on:
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:9000/ || exit 1"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
|
||||
producer_raw_recipes:
|
||||
container_name: producer-raw-recipes
|
||||
build:
|
||||
context: ./producer-raw-recipes
|
||||
networks:
|
||||
- kafka-network
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
depends_on:
|
||||
kafdrop:
|
||||
condition: service_healthy
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
zookeeper:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/kafka/9092' || exit 1"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
|
||||
|
||||
producer_consumer_parse_recipes:
|
||||
container_name: producer-consumer-parse-recipes
|
||||
build:
|
||||
context: ./producer-consumer-parse-recipes
|
||||
networks:
|
||||
- kafka-network
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
depends_on:
|
||||
kafdrop:
|
||||
condition: service_healthy
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
zookeeper:
|
||||
condition: service_healthy
|
||||
producer_raw_recipes:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/kafka/9092' || exit 1"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
|
||||
consumer_notification:
|
||||
container_name: consumer-notification
|
||||
build:
|
||||
context: ./consumer-notification
|
||||
networks:
|
||||
- kafka-network
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
depends_on:
|
||||
kafdrop:
|
||||
condition: service_healthy
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
zookeeper:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/kafka/9092' || exit 1"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
|
||||
networks:
|
||||
kafka-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
zookeeper_data:
|
||||
driver: local
|
||||
kafka_data:
|
||||
driver: local
|
||||
@@ -0,0 +1,17 @@
|
||||
# Usa una imagen base de Python
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Configura el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia el archivo de requisitos al contenedor
|
||||
COPY requirements.txt /app/
|
||||
|
||||
# Instala las dependencias
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el script de Python al contenedor
|
||||
COPY producer-consumer-parse-recipes.py /app/
|
||||
|
||||
# Comando por defecto para ejecutar el script
|
||||
CMD ["python", "producer-consumer-parse-recipes.py"]
|
||||
@@ -0,0 +1,165 @@
|
||||
import json
|
||||
import logging
|
||||
from time import sleep
|
||||
from bs4 import BeautifulSoup
|
||||
from kafka import KafkaConsumer, KafkaProducer
|
||||
|
||||
# Configuración del registro
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
|
||||
def publish_message(producer_instance, topic_name, key, value):
|
||||
"""
|
||||
Publica un mensaje en el tema de Kafka especificado.
|
||||
"""
|
||||
|
||||
try:
|
||||
key_bytes = bytes(key, encoding='utf-8') # Convertir clave a bytes
|
||||
value_bytes = bytes(value, encoding='utf-8') # Convertir valor a bytes
|
||||
producer_instance.send( # Enviar mensaje
|
||||
topic_name, key=key_bytes,
|
||||
value=value_bytes
|
||||
)
|
||||
producer_instance.flush() # Asegurar que el mensaje ha sido enviado
|
||||
|
||||
logging.info('[i] Mensaje publicado con éxito.')
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error('[!] Error al publicar mensaje', exc_info=True)
|
||||
|
||||
|
||||
def connect_kafka_producer():
|
||||
"""
|
||||
Conecta y devuelve una instancia del productor de Kafka.
|
||||
"""
|
||||
|
||||
try:
|
||||
producer = KafkaProducer(
|
||||
bootstrap_servers=['kafka:9092'], # Servidor Kafka
|
||||
api_version=(0, 10) # Versión de la API de Kafka
|
||||
)
|
||||
|
||||
logging.info('[i] Conectado con éxito al productor de Kafka.')
|
||||
|
||||
return producer
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error('[!] Error al conectar con Kafka', exc_info=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse(markup):
|
||||
"""
|
||||
Analiza el HTML y extrae la información de la receta.
|
||||
"""
|
||||
|
||||
title = '-'
|
||||
submit_by = '-'
|
||||
description = '-'
|
||||
calories = 0
|
||||
ingredients = []
|
||||
rec = {}
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(markup, 'lxml') # Analizar HTML con BeautifulSoup
|
||||
|
||||
# Actualizar selectores CSS para el título, descripción, ingredientes y calorías
|
||||
title_section = soup.select_one('h1.headline.heading-content') # Título
|
||||
submitter_section = soup.select_one('span.author-name') # Autor
|
||||
description_section = soup.select_one('div.recipe-summary > p') # Descripción
|
||||
ingredients_section = soup.select('li.ingredients-item') # Ingredientes
|
||||
calories_section = soup.select_one('span.calorie-count') # Calorías
|
||||
|
||||
# Extraer calorías
|
||||
if calories_section:
|
||||
calories = calories_section.get_text(strip=True).replace('cals', '').strip()
|
||||
|
||||
# Extraer ingredientes
|
||||
if ingredients_section:
|
||||
for ingredient in ingredients_section:
|
||||
ingredient_text = ingredient.get_text(strip=True)
|
||||
if 'Add all ingredients to list' not in ingredient_text and ingredient_text != '':
|
||||
ingredients.append({'step': ingredient_text})
|
||||
|
||||
# Extraer descripción
|
||||
if description_section:
|
||||
description = description_section.get_text(strip=True)
|
||||
|
||||
# Extraer nombre del autor
|
||||
if submitter_section:
|
||||
submit_by = submitter_section.get_text(strip=True)
|
||||
|
||||
# Extraer título
|
||||
if title_section:
|
||||
title = title_section.get_text(strip=True)
|
||||
|
||||
# Crear diccionario con la información de la receta
|
||||
rec = {
|
||||
'title': title,
|
||||
'submitter': submit_by,
|
||||
'description': description,
|
||||
'calories': calories,
|
||||
'ingredients': ingredients
|
||||
}
|
||||
|
||||
logging.info(f"[i] Receta extraída: {rec}")
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error('[!] Error en parsing', exc_info=True)
|
||||
|
||||
return json.dumps(rec)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Ejecuta el proceso de consumo y publicación de mensajes.
|
||||
"""
|
||||
|
||||
topic_name = 'raw_recipes'
|
||||
parsed_topic_name = 'parsed_recipes'
|
||||
parsed_records = []
|
||||
|
||||
# Crear un consumidor de Kafka
|
||||
consumer = KafkaConsumer(
|
||||
topic_name,
|
||||
auto_offset_reset='earliest',
|
||||
bootstrap_servers=['kafka:9092'],
|
||||
api_version=(0, 10),
|
||||
consumer_timeout_ms=2000
|
||||
)
|
||||
|
||||
logging.info('[i] Iniciando el consumidor para parsing...')
|
||||
|
||||
try:
|
||||
for msg in consumer:
|
||||
html = msg.value
|
||||
result = parse(html) # Analizar el HTML
|
||||
parsed_records.append(result)
|
||||
|
||||
consumer.close() # Cerrar el consumidor
|
||||
|
||||
logging.info('[i] Consumidor cerrado.')
|
||||
|
||||
if parsed_records:
|
||||
logging.info('[+] Publicando registros...')
|
||||
producer = connect_kafka_producer()
|
||||
|
||||
if producer:
|
||||
for rec in parsed_records:
|
||||
publish_message(producer, parsed_topic_name, 'parsed', rec)
|
||||
|
||||
producer.close() # Cerrar el productor
|
||||
else:
|
||||
logging.error('[!] El productor de Kafka no está disponible.')
|
||||
|
||||
except Exception as ex:
|
||||
logging.error('[!] Error en el productor-consumer', exc_info=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
kafka-python
|
||||
requests
|
||||
beautifulsoup4
|
||||
@@ -0,0 +1,17 @@
|
||||
# Usa una imagen base de Python
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Configura el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia el archivo de requisitos al contenedor
|
||||
COPY requirements.txt /app/
|
||||
|
||||
# Instala las dependencias
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el script de Python al contenedor
|
||||
COPY producer-raw-recipes.py /app/
|
||||
|
||||
# Comando por defecto para ejecutar el script
|
||||
CMD ["python", "producer-raw-recipes.py"]
|
||||
@@ -0,0 +1,168 @@
|
||||
import requests
|
||||
from time import sleep
|
||||
import logging
|
||||
from bs4 import BeautifulSoup
|
||||
from kafka import KafkaProducer
|
||||
|
||||
|
||||
# Configuración del registro
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
|
||||
def publish_message(producer_instance, topic_name, key, value):
|
||||
"""
|
||||
Publica un mensaje en el tema de Kafka especificado.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
key_bytes = bytes(key, encoding='utf-8') # Convertir clave a bytes
|
||||
value_bytes = bytes(value, encoding='utf-8') # Convertir valor a bytes
|
||||
producer_instance.send( # Enviar mensaje
|
||||
topic_name, key=key_bytes, value=value_bytes
|
||||
)
|
||||
producer_instance.flush() # Asegurar que el mensaje ha sido enviado
|
||||
logging.info('[+] Mensaje publicado con éxito.')
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error('[!] Error al publicar mensaje', exc_info=True)
|
||||
|
||||
|
||||
def connect_kafka_producer():
|
||||
"""
|
||||
Conecta y devuelve una instancia del productor de Kafka.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
producer = KafkaProducer(
|
||||
bootstrap_servers=['kafka:9092'], # Servidor Kafka
|
||||
api_version=(0, 10) # Versión de la API de Kafka
|
||||
)
|
||||
|
||||
logging.info('[i] Conectado con éxito al productor de Kafka.')
|
||||
|
||||
return producer
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error('[!] Error al conectar con Kafka', exc_info=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def fetch_raw(recipe_url):
|
||||
"""
|
||||
Obtiene el HTML sin procesar de la URL de la receta.
|
||||
"""
|
||||
|
||||
html = None
|
||||
logging.info('[i] Procesando... {}'.format(recipe_url))
|
||||
|
||||
try:
|
||||
|
||||
r = requests.get(recipe_url, headers=headers)
|
||||
|
||||
if r.status_code == 200:
|
||||
html = r.text
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
logging.error(
|
||||
'[!] Error al acceder al HTML sin procesar',
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return html.strip() if html else ''
|
||||
|
||||
|
||||
def get_recipes():
|
||||
"""
|
||||
Obtiene una lista de recetas de la URL de origen.
|
||||
"""
|
||||
|
||||
recipes = []
|
||||
url = 'https://www.allrecipes.com/recipes/96/salad/'
|
||||
|
||||
logging.info('[i] Accediendo a la lista de recetas...')
|
||||
|
||||
try:
|
||||
r = requests.get(url, headers=headers)
|
||||
logging.info('[i] Código de respuesta: {}'.format(r.status_code))
|
||||
|
||||
if r.status_code == 200:
|
||||
logging.info('[i] Página accesible, procesando...')
|
||||
|
||||
html = r.text
|
||||
soup = BeautifulSoup(html, 'lxml')
|
||||
|
||||
# Selecciona los elementos <a> con la clase mntl-card-list-items
|
||||
links = soup.select('a.mntl-card-list-items')
|
||||
|
||||
logging.info('[i] Se encontraron {} recetas'.format(len(links)))
|
||||
|
||||
for link in links:
|
||||
# Obtiene el título del texto de card__title-text
|
||||
recipe_title = link.select_one(
|
||||
'.card__title-text').get_text(strip=True)
|
||||
recipe_url = link['href']
|
||||
logging.info(
|
||||
f'[i] Procesando receta: {recipe_title}, enlace: {recipe_url}'
|
||||
)
|
||||
|
||||
sleep(2)
|
||||
recipe_html = fetch_raw(recipe_url)
|
||||
|
||||
if recipe_html:
|
||||
recipes.append(recipe_html)
|
||||
|
||||
logging.info(
|
||||
'[i] Se obtuvieron {} recetas en total.'.format(len(recipes)))
|
||||
|
||||
else:
|
||||
logging.error(
|
||||
'[!] No se pudo acceder a la página de recetas, código de respuesta: {}'.format(r.status_code))
|
||||
|
||||
except Exception as ex:
|
||||
logging.error('[!] Error en get_recipes', exc_info=True)
|
||||
|
||||
return recipes
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Ejecuta el proceso de obtención y publicación de recetas.
|
||||
"""
|
||||
|
||||
global headers
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
|
||||
logging.info('Iniciando el productor de recetas...')
|
||||
|
||||
all_recipes = get_recipes() # Obtener recetas
|
||||
|
||||
if all_recipes:
|
||||
|
||||
producer = connect_kafka_producer() # Conectar con Kafka
|
||||
|
||||
if producer:
|
||||
|
||||
for recipe in all_recipes:
|
||||
|
||||
publish_message(producer, 'raw_recipes', 'raw', recipe.strip())
|
||||
|
||||
producer.close() # Cerrar el productor
|
||||
|
||||
else:
|
||||
|
||||
logging.error('[!] El productor de Kafka no está disponible.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
kafka-python
|
||||
requests
|
||||
beautifulsoup4
|
||||
lxml
|
||||
191
catch-all/05_infra_test/04_elastic_stack/README.md
Normal file
191
catch-all/05_infra_test/04_elastic_stack/README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
|
||||
# Cómo Usar Elasticsearch en Python con Docker
|
||||
|
||||
|
||||
|
||||
**Índice**
|
||||
- [Cómo Usar Elasticsearch en Python con Docker](#cómo-usar-elasticsearch-en-python-con-docker)
|
||||
- [¿Qué es Elasticsearch?](#qué-es-elasticsearch)
|
||||
- [Requisitos Previos](#requisitos-previos)
|
||||
- [Crear un Clúster Local del Elastic Stack con Docker Compose](#crear-un-clúster-local-del-elastic-stack-con-docker-compose)
|
||||
- [Desplegar el Elastic Stack](#desplegar-el-elastic-stack)
|
||||
- [Dockerizar el Programa Python](#dockerizar-el-programa-python)
|
||||
- [Programa Python](#programa-python)
|
||||
- [Conectar a Tu Clúster desde el Contenedor Python](#conectar-a-tu-clúster-desde-el-contenedor-python)
|
||||
- [Ejecutar los Scripts Python en Docker](#ejecutar-los-scripts-python-en-docker)
|
||||
- [Construir y Ejecutar el Contenedor Python](#construir-y-ejecutar-el-contenedor-python)
|
||||
- [Visualización de Datos con Kibana](#visualización-de-datos-con-kibana)
|
||||
- [Usar Logstash para Ingestión de Datos](#usar-logstash-para-ingestión-de-datos)
|
||||
- [Recopilar Métricas con Metricbeat](#recopilar-métricas-con-metricbeat)
|
||||
- [Eliminar Documentos del Índice](#eliminar-documentos-del-índice)
|
||||
- [Eliminar un Índice](#eliminar-un-índice)
|
||||
- [Conclusión](#conclusión)
|
||||
|
||||
[Elasticsearch](https://www.elastic.co/elasticsearch/) (ES) es una tecnología utilizada por muchas empresas, incluyendo GitHub, Uber y Facebook. Aunque no se enseña con frecuencia en cursos de Ciencia de Datos, es probable que te encuentres con ella en tu carrera profesional.
|
||||
|
||||
Muchos científicos de datos enfrentan dificultades al configurar un entorno local o al intentar interactuar con Elasticsearch en Python. Además, no hay muchos recursos actualizados disponibles.
|
||||
|
||||
Por eso, decidí crear este tutorial. Te enseñará lo básico y podrás configurar un clúster de Elasticsearch en tu máquina para el desarrollo local rápidamente. También aprenderás a crear un índice, almacenar datos en él y usarlo para buscar información.
|
||||
|
||||
¡Vamos a empezar!
|
||||
|
||||
|
||||
## ¿Qué es Elasticsearch?
|
||||
|
||||
Elasticsearch es un motor de búsqueda distribuido, rápido y fácil de escalar, capaz de manejar datos textuales, numéricos, geoespaciales, estructurados y no estructurados. Es un motor de búsqueda popular para aplicaciones, sitios web y análisis de registros. También es un componente clave del Elastic Stack (también conocido como ELK Stack), que incluye Logstash y Kibana, junto con Beats para la recolección de datos.
|
||||
|
||||
Para entender el funcionamiento interno de Elasticsearch, piensa en él como dos procesos distintos. Uno es la ingestión, que normaliza y enriquece los datos brutos antes de indexarlos usando un [índice invertido](https://www.elastic.co/guide/en/elasticsearch/reference/current/documents-indices.html). El segundo es la recuperación, que permite a los usuarios recuperar datos escribiendo consultas que se ejecutan contra el índice.
|
||||
|
||||
Eso es todo lo que necesitas saber por ahora. A continuación, prepararás tu entorno local para ejecutar un clúster del Elastic Stack.
|
||||
|
||||
## Requisitos Previos
|
||||
|
||||
Debes configurar algunas cosas antes de comenzar. Asegúrate de tener lo siguiente listo:
|
||||
|
||||
1. Instala Docker y Docker Compose.
|
||||
2. Descarga los datos necesarios.
|
||||
|
||||
## Crear un Clúster Local del Elastic Stack con Docker Compose
|
||||
|
||||
Para desplegar el Elastic Stack (Elasticsearch, Kibana, Logstash y Beats) en tu máquina local, utilizaremos Docker Compose. Este enfoque simplifica el despliegue y la administración de múltiples servicios.
|
||||
|
||||
Mediante el fichero [docker-compose.yaml](docker-compose.yaml) vamos a configurar los servicios para Elasticsearch, Kibana, Logstash, Metricbeat, y la aplicación Python, todos conectados a una red llamada `elastic`.
|
||||
|
||||
|
||||
### Desplegar el Elastic Stack
|
||||
|
||||
Para iniciar los servicios, abre una terminal en el directorio donde se encuentra `docker-compose.yml` y ejecuta el siguiente comando:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Este comando iniciará Elasticsearch, Kibana, Logstash, Metricbeat y la aplicación Python en contenedores separados. Aquí tienes un desglose de los servicios:
|
||||
|
||||
- **Elasticsearch**: El motor de búsqueda que almacena y analiza los datos.
|
||||
- **Kibana**: Una interfaz de usuario para visualizar y explorar datos en Elasticsearch.
|
||||
- **Logstash**: Una herramienta de procesamiento de datos que ingiere datos desde múltiples fuentes, los transforma y los envía a un destino como Elasticsearch.
|
||||
- **Metricbeat**: Un agente de monitoreo que recopila métricas del sistema y las envía a Elasticsearch.
|
||||
- **Python App**: Un contenedor que ejecutará tus scripts de Python para interactuar con Elasticsearch. (Hasta que no construyamos el contenedor, este servicio fallará).
|
||||
|
||||
|
||||
## Dockerizar el Programa Python
|
||||
|
||||
Para dockerizar la aplicación Python, utilizaremos el archivo [Dockerfile](./app/Dockerfile) del directorio `app`. El directorio `app` también debe contener el programa Python y un archivo [requirements.txt](./app/requirements.txt) para manejar las dependencias.
|
||||
|
||||
### Programa Python
|
||||
|
||||
El archivo [main.py](./app/main.py) en el directorio `app` manejará la conexión a Elasticsearch, la creación de índices y la carga de datos.
|
||||
|
||||
Este programa realiza las siguientes acciones:
|
||||
|
||||
1. **Crea un índice** en Elasticsearch con las configuraciones de mapeo necesarias.
|
||||
2. **Carga datos** desde un archivo CSV (`wiki_movie_plots_deduped.csv`) al índice creado.
|
||||
|
||||
|
||||
## Conectar a Tu Clúster desde el Contenedor Python
|
||||
|
||||
Tu aplicación Python se conectará al clúster de Elasticsearch usando el hostname `elasticsearch`, que es el nombre del servicio definido en `docker-compose.yml`.
|
||||
|
||||
|
||||
## Ejecutar los Scripts Python en Docker
|
||||
|
||||
Una vez que hayas creado los archivos `Dockerfile`, `requirements.txt` y `main.py`, puedes construir la imagen de Docker para tu aplicación Python y ejecutarla usando Docker Compose.
|
||||
|
||||
|
||||
### Construir y Ejecutar el Contenedor Python
|
||||
|
||||
1. Construye la imagen de Docker para tu aplicación Python:
|
||||
|
||||
```bash
|
||||
docker compose build python-app
|
||||
```
|
||||
|
||||
2. Ejecuta el contenedor:
|
||||
|
||||
```bash
|
||||
docker compose up python-app
|
||||
```
|
||||
|
||||
La aplicación Python se ejecutará y cargará los datos en Elasticsearch. Puedes verificar que los datos se hayan indexado correctamente ejecutando una consulta en Elasticsearch o usando Kibana para explorar los datos:
|
||||
|
||||
```python
|
||||
es.search(index="movies", body={"query": {"match_all": {}}})
|
||||
```
|
||||
|
||||
|
||||
## Visualización de Datos con Kibana
|
||||
|
||||
Kibana es una herramienta de visualización que se conecta a Elasticsearch y te permite explorar y visualizar tus datos.
|
||||
|
||||
Para acceder a Kibana, abre un navegador web y navega a `http://localhost:5601`. Deberías ver la interfaz de Kibana, donde puedes crear visualizaciones y dashboards.
|
||||
|
||||
1. **Crea un índice en Kibana**: Ve a *Management > Index Patterns* y crea un nuevo patrón de índice para el índice `movies`.
|
||||
2. **Explora tus datos**: Usa la herramienta *Discover* para buscar y explorar los datos que has indexado.
|
||||
3. **Crea visualizaciones**: En la sección *Visualize*, crea gráficos y tablas que te permitan entender mejor tus datos.
|
||||
|
||||
|
||||
## Usar Logstash para Ingestión de Datos
|
||||
|
||||
Logstash es una herramienta para procesar y transformar datos antes de enviarlos a Elasticsearch. Aquí tienes un ejemplo básico de cómo configurar Logstash para que procese y envíe datos a Elasticsearch.
|
||||
|
||||
Mediante el archivo de configuración en la carpeta `logstash-config/` llamado [pipeline.conf](./logstash-config/pipeline.conf) realizaremos los siguientes pasos:
|
||||
|
||||
- Lee un archivo CSV de entrada.
|
||||
- Usa el filtro `csv` para descomponer cada línea en campos separados.
|
||||
- Usa `mutate` para convertir el año de lanzamiento en un número entero.
|
||||
- Envía los datos procesados a Elasticsearch.
|
||||
|
||||
Para usar Logstash con esta configuración, asegúrate de que el archivo `wiki_movie_plots_deduped.csv` esté accesible en tu sistema y modifica la ruta en el archivo de configuración según sea necesario. Luego, reinicia el contenedor de Logstash para aplicar los cambios.
|
||||
|
||||
```bash
|
||||
docker compose restart logstash
|
||||
```
|
||||
|
||||
|
||||
## Recopilar Métricas con Metricbeat
|
||||
|
||||
Metricbeat es un agente ligero que recopila métricas del sistema y las envía a Elasticsearch. Está configurado en el archivo `docker-compose.yml` que has creado anteriormente.
|
||||
|
||||
Para ver las métricas en Kibana:
|
||||
|
||||
1. **Configura Metricbeat**: Edita el archivo de configuración de Metricbeat si necesitas recopilar métricas específicas.
|
||||
2. **Importa dashboards preconfigurados**: En Kibana, navega a *Add Data > Metricbeat* y sigue las instrucciones para importar los dashboards preconfigurados.
|
||||
3. **Explora las métricas**: Usa los dashboards importados para explorar las métricas de tu sistema.
|
||||
|
||||
|
||||
## Eliminar Documentos del Índice
|
||||
|
||||
Puedes usar el siguiente código para eliminar documentos del índice:
|
||||
|
||||
```python
|
||||
es.delete(index="movies", id="2500")
|
||||
```
|
||||
|
||||
El código anterior eliminará el documento con ID 2500 del índice `movies`.
|
||||
|
||||
|
||||
## Eliminar un Índice
|
||||
|
||||
Finalmente, si por alguna razón deseas eliminar un índice (y todos sus documentos), aquí te mostramos cómo hacerlo:
|
||||
|
||||
```python
|
||||
es.indices.delete(index='movies')
|
||||
```
|
||||
|
||||
|
||||
## Conclusión
|
||||
|
||||
Este tutorial te enseñó los conceptos básicos de Elasticsearch y cómo usarlo junto con el Elastic Stack y Docker. Esto será útil en tu carrera, ya que seguramente te encontrarás con Elasticsearch en algún momento.
|
||||
|
||||
En este tutorial, has aprendido:
|
||||
|
||||
- Cómo configurar un clúster del Elastic Stack en tu máquina usando Docker Compose.
|
||||
- Cómo dockerizar una aplicación Python para interactuar con Elasticsearch.
|
||||
- Cómo crear un índice y almacenar datos en él.
|
||||
- Cómo buscar tus datos usando Elasticsearch.
|
||||
- Cómo visualizar datos con Kibana.
|
||||
- Cómo procesar datos con Logstash.
|
||||
- Cómo recopilar métricas con Metricbeat.
|
||||
|
||||
Explora más funcionalidades de Elasticsearch y el Elastic Stack para sacar el máximo provecho de tus datos.
|
||||
15
catch-all/05_infra_test/04_elastic_stack/app/Dockerfile
Normal file
15
catch-all/05_infra_test/04_elastic_stack/app/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
# Usa la imagen base de Python
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Establece el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia el archivo requirements.txt e instala las dependencias
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el código fuente a /app
|
||||
COPY . /app/
|
||||
|
||||
# Comando para ejecutar la aplicación
|
||||
CMD ["python", "main.py"]
|
||||
114
catch-all/05_infra_test/04_elastic_stack/app/main.py
Normal file
114
catch-all/05_infra_test/04_elastic_stack/app/main.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import pandas as pd
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch.helpers import bulk
|
||||
|
||||
# Configura la conexión a Elasticsearch
|
||||
es = Elasticsearch("http://elasticsearch:9200")
|
||||
|
||||
|
||||
def create_index():
|
||||
"""
|
||||
Crea un índice en Elasticsearch con el nombre 'movies' si no existe.
|
||||
Define el mapeo del índice para los campos de los documentos.
|
||||
"""
|
||||
|
||||
# Define el mapeo del índice 'movies'
|
||||
mappings = {
|
||||
"properties": {
|
||||
# Campo para el título de la película
|
||||
"title": {"type": "text", "analyzer": "english"},
|
||||
# Campo para la etnicidad
|
||||
"ethnicity": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el director
|
||||
"director": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el elenco
|
||||
"cast": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el género
|
||||
"genre": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el argumento de la película
|
||||
"plot": {"type": "text", "analyzer": "english"},
|
||||
# Campo para el año de lanzamiento
|
||||
"year": {"type": "integer"},
|
||||
# Campo para la página de Wikipedia
|
||||
"wiki_page": {"type": "keyword"}
|
||||
}
|
||||
}
|
||||
|
||||
# Verifica si el índice 'movies' ya existe
|
||||
if not es.indices.exists(index="movies"):
|
||||
|
||||
# Crea el índice 'movies' si no existe
|
||||
es.indices.create(index="movies", mappings=mappings)
|
||||
print("\n[+] Índice 'movies' creado.")
|
||||
|
||||
else:
|
||||
|
||||
print("\n[!] El índice 'movies' ya existe.")
|
||||
|
||||
|
||||
def load_data():
|
||||
"""
|
||||
Carga datos desde un archivo CSV a Elasticsearch.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
# Lee el archivo CSV
|
||||
df = pd.read_csv("/app/wiki_movie_plots_deduped.csv", quoting=1)
|
||||
|
||||
# Verifica el número de filas en el DataFrame
|
||||
num_rows = len(df)
|
||||
sample_size = min(5000, num_rows)
|
||||
|
||||
# Elimina filas con valores nulos y toma una muestra
|
||||
df = df.dropna().sample(sample_size, random_state=42).reset_index(drop=True)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f"\n[!] Error al leer el archivo CSV: {e}")
|
||||
|
||||
return
|
||||
|
||||
# Prepara los datos para la carga en Elasticsearch
|
||||
bulk_data = [
|
||||
{
|
||||
"_index": "movies", # Nombre del índice en Elasticsearch
|
||||
"_id": i, # ID del documento en Elasticsearch
|
||||
"_source": {
|
||||
"title": row["Title"], # Título de la película
|
||||
"ethnicity": row["Origin/Ethnicity"], # Etnicidad
|
||||
"director": row["Director"], # Director
|
||||
"cast": row["Cast"], # Elenco
|
||||
"genre": row["Genre"], # Género
|
||||
"plot": row["Plot"], # Argumento
|
||||
"year": row["Release Year"], # Año de lanzamiento
|
||||
"wiki_page": row["Wiki Page"], # Página de Wikipedia
|
||||
}
|
||||
}
|
||||
for i, row in df.iterrows() # Itera sobre cada fila del DataFrame
|
||||
]
|
||||
|
||||
try:
|
||||
|
||||
# Carga los datos en Elasticsearch en bloques
|
||||
bulk(es, bulk_data)
|
||||
print("\n[+] Datos cargados en Elasticsearch.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f"\n[!] Error al cargar datos en Elasticsearch: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Función principal que crea el índice y carga los datos.
|
||||
"""
|
||||
|
||||
create_index() # Crea el índice en Elasticsearch
|
||||
load_data() # Carga los datos en Elasticsearch
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Ejecuta la función principal si el script se ejecuta directamente
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
pandas==2.0.1
|
||||
numpy==1.24.2
|
||||
elasticsearch==8.8.0
|
||||
@@ -0,0 +1,7 @@
|
||||
Title,Origin/Ethnicity,Director,Cast,Genre,Plot,Release Year,Wiki Page
|
||||
The Shawshank Redemption,American,Frank Darabont,"Tim Robbins, Morgan Freeman",Drama,"Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.",1994,https://en.wikipedia.org/wiki/The_Shawshank_Redemption
|
||||
The Godfather,American,Francis Ford Coppola,"Marlon Brando, Al Pacino",Crime,"The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",1972,https://en.wikipedia.org/wiki/The_Godfather
|
||||
The Dark Knight,American,Christopher Nolan,"Christian Bale, Heath Ledger",Action,"When the menace known as the Joker emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham.",2008,https://en.wikipedia.org/wiki/The_Dark_Knight
|
||||
Pulp Fiction,American,Quentin Tarantino,"John Travolta, Uma Thurman",Crime,"The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.",1994,https://en.wikipedia.org/wiki/Pulp_Fiction
|
||||
The Lord of the Rings: The Return of the King,American,Peter Jackson,"Elijah Wood, Viggo Mortensen",Fantasy,"The final battle for Middle-earth begins. The forces of good and evil are drawn into a confrontation and the outcome will determine the fate of the world.",2003,https://en.wikipedia.org/wiki/The_Lord_of_the_Rings:_The_Return_of_the_King
|
||||
Inception,American,Christopher Nolan,"Leonardo DiCaprio, Joseph Gordon-Levitt",Sci-Fi,"A thief who enters the dreams of others to steal secrets from their subconscious is given the inverse task of planting an idea into the mind of a CEO.",2010,https://en.wikipedia.org/wiki/Inception
|
||||
|
72
catch-all/05_infra_test/04_elastic_stack/docker-compose.yaml
Normal file
72
catch-all/05_infra_test/04_elastic_stack/docker-compose.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.15.2
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
networks:
|
||||
- elastic
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
kibana:
|
||||
image: docker.elastic.co/kibana/kibana:7.15.2
|
||||
container_name: kibana
|
||||
environment:
|
||||
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
|
||||
ports:
|
||||
- "5601:5601"
|
||||
networks:
|
||||
- elastic
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
logstash:
|
||||
image: docker.elastic.co/logstash/logstash:7.15.2
|
||||
container_name: logstash
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ./logstash-config/:/usr/share/logstash/pipeline/
|
||||
environment:
|
||||
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "9600:9600"
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
metricbeat:
|
||||
image: docker.elastic.co/beats/metricbeat:7.15.2
|
||||
container_name: metricbeat
|
||||
command: metricbeat -e -E output.elasticsearch.hosts=["http://elasticsearch:9200"]
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
networks:
|
||||
- elastic
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
python-app:
|
||||
build:
|
||||
context: ./app
|
||||
dockerfile: Dockerfile
|
||||
container_name: python-app
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ./app:/app
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
networks:
|
||||
elastic:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,26 @@
|
||||
input {
|
||||
file {
|
||||
path => "/path/to/your/data/wiki_movie_plots_deduped.csv"
|
||||
start_position => "beginning"
|
||||
sincedb_path => "/dev/null"
|
||||
}
|
||||
}
|
||||
|
||||
filter {
|
||||
csv {
|
||||
separator => ","
|
||||
columns => ["Release Year", "Title", "Origin/Ethnicity", "Director", "Cast", "Genre", "Wiki Page", "Plot"]
|
||||
}
|
||||
|
||||
mutate {
|
||||
convert => { "Release Year" => "integer" }
|
||||
}
|
||||
}
|
||||
|
||||
output {
|
||||
elasticsearch {
|
||||
hosts => ["http://elasticsearch:9200"]
|
||||
index => "movies"
|
||||
}
|
||||
stdout { codec => rubydebug }
|
||||
}
|
||||
19
catch-all/05_infra_test/05_prometheus_grafana/Dockerfile
Normal file
19
catch-all/05_infra_test/05_prometheus_grafana/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Usar imagen de python 3.9 slim
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Establece el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia el archivo requirements.txt e instala las dependencias
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el código fuente a /app
|
||||
COPY app app
|
||||
|
||||
# Variables de entorno
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# Comando para ejecutar la aplicación
|
||||
CMD ["python", "app/main.py"]
|
||||
251
catch-all/05_infra_test/05_prometheus_grafana/README.md
Normal file
251
catch-all/05_infra_test/05_prometheus_grafana/README.md
Normal file
@@ -0,0 +1,251 @@
|
||||
|
||||
# Configuración de Grafana con Prometheus para proyectos Python usando Docker
|
||||
|
||||
En este artículo, cubriremos cómo configurar el monitoreo de servicios para proyectos Python con Prometheus y Grafana utilizando contenedores Docker.
|
||||
|
||||
El monitoreo de servicios nos permite analizar eventos específicos en nuestros proyectos, tales como llamadas a bases de datos, interacción con API, seguimiento del rendimiento de recursos, etc. Puedes detectar fácilmente comportamientos inusuales o descubrir pistas útiles detrás de los problemas.
|
||||
|
||||
## Escenario de Caso Real
|
||||
|
||||
Teníamos un servicio temporal que redirigía las solicitudes entrantes de sitios web específicos hasta que Google dejara de indexar estas páginas web. Utilizando el monitoreo de servicios, podemos ver fácilmente el conteo de redirecciones de forma regular. En un momento determinado en el futuro, el número de redirecciones disminuirá, lo que significa que el tráfico se ha migrado al sitio web objetivo, y ya no necesitaremos que este servicio esté en funcionamiento.
|
||||
|
||||
## Configuración de contenedores Docker
|
||||
|
||||
Vamos a ejecutar todos nuestros servicios localmente en contenedores Docker. En las grandes empresas, existe un servicio global para Prometheus y Grafana que incluye todos los proyectos de monitoreo de microservicios. Probablemente ni siquiera necesites escribir ningún pipeline de despliegue para las herramientas de monitoreo de servicios.
|
||||
|
||||
### Archivo docker-compose.yml
|
||||
|
||||
Comencemos creando un archivo `docker-compose.yml` con los servicios necesarios:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ${PWD}/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:9090/-/healthy"]
|
||||
interval: 1m30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
grafana:
|
||||
hostname: grafana
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
ports:
|
||||
- 3001:3000
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 1m30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
- prometheus
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- monitoring-net
|
||||
command: ["python3", "app/main.py"]
|
||||
|
||||
networks:
|
||||
monitoring-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
El punto más importante de la configuración anterior es montar el archivo `prometheus.yml` desde nuestra máquina local al contenedor Docker. Este archivo incluye la configuración para obtener datos (métricas) de nuestro servicio de aplicación o proyecto Python. Sin el archivo, no podrás ver las métricas personalizadas que incluye tu proyecto.
|
||||
|
||||
Por lo tanto, crea un nuevo archivo llamado `prometheus.yml` en el nivel raíz de tu proyecto.
|
||||
|
||||
### Archivo prometheus.yml
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'app'
|
||||
static_configs:
|
||||
- targets: ['app:8080']
|
||||
```
|
||||
|
||||
Ahora, Prometheus obtendrá datos de nuestro proyecto.
|
||||
|
||||
Todas las demás configuraciones en el archivo de composición son autoexplicativas y no son muy críticas, como mencionamos para Prometheus.
|
||||
|
||||
## Crear un nuevo proyecto Python
|
||||
|
||||
Ahora, vamos a crear una aplicación Python muy sencilla que creará una métrica para rastrear el tiempo empleado y las solicitudes realizadas. Crea una nueva carpeta llamada `app` en el nivel raíz del proyecto. También incluye `__init__.py` para marcarla como un paquete Python.
|
||||
|
||||
A continuación, crea otro archivo llamado `main.py` que contendrá la lógica principal del programa, como se muestra a continuación:
|
||||
|
||||
### Archivo app/main.py
|
||||
|
||||
```python
|
||||
from prometheus_client import start_http_server, Summary, Counter, Gauge, Histogram
|
||||
import random
|
||||
import time
|
||||
|
||||
# Create metrics to track time spent, requests made, and other events.
|
||||
REQUEST_TIME = Summary('request_processing_seconds', 'Time spent processing request')
|
||||
UPDATE_COUNT = Counter('update_count', 'Number of updates')
|
||||
ACTIVE_REQUESTS = Gauge('active_requests', 'Number of active requests')
|
||||
REQUEST_LATENCY = Histogram('request_latency_seconds', 'Request latency in seconds')
|
||||
|
||||
# Decorate function with metrics.
|
||||
@REQUEST_TIME.time()
|
||||
@REQUEST_LATENCY.time()
|
||||
def process_request(t):
|
||||
"""A dummy function that takes some time."""
|
||||
ACTIVE_REQUESTS.inc()
|
||||
time.sleep(t)
|
||||
ACTIVE_REQUESTS.dec()
|
||||
|
||||
def main():
|
||||
# Start up the server to expose the metrics.
|
||||
start_http_server(8000)
|
||||
print("[*] Starting server on port 8000...")
|
||||
|
||||
# Generate some requests.
|
||||
while True:
|
||||
msg = random.random()
|
||||
process_request(msg)
|
||||
update_increment = random.randint(1, 100)
|
||||
UPDATE_COUNT.inc(update_increment)
|
||||
print(f'[+] Processing request: {msg:.4f} | Updates: {update_increment}')
|
||||
time.sleep(random.uniform(0.5, 2.0)) # Random delay between requests
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
Aquí, estamos utilizando un paquete de Python llamado `prometheus_client` para interactuar con Prometheus. Permite fácilmente la creación de diferentes tipos de métricas que nuestro proyecto requiere.
|
||||
|
||||
El código anterior se copió de la documentación oficial de `prometheus_client`, que simplemente crea una nueva métrica llamada `request_processing_seconds` que mide el tiempo empleado en esa solicitud en particular. Cubriremos otros tipos de métricas más adelante en este artículo.
|
||||
|
||||
Ahora, vamos a crear un `Dockerfile` y un `requirements.txt` para construir nuestro proyecto.
|
||||
|
||||
### Archivo Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# Usa la imagen base oficial de Python
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Establece el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia los archivos de requerimientos
|
||||
COPY requirements.txt .
|
||||
|
||||
# Instala las dependencias
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el código de la aplicación
|
||||
COPY app/ /app
|
||||
|
||||
# Expone el puerto de la aplicación
|
||||
EXPOSE 8000
|
||||
|
||||
# Comando por defecto para ejecutar la aplicación
|
||||
CMD ["python", "main.py"]
|
||||
```
|
||||
|
||||
### Archivo requirements.txt
|
||||
|
||||
```txt
|
||||
prometheus_client
|
||||
```
|
||||
|
||||
Ahora estamos listos para construir y ejecutar los servicios.
|
||||
|
||||
## Ejecución de los servicios
|
||||
|
||||
Para ver todo en acción, ejecuta los siguientes comandos:
|
||||
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
Esto levantará todos los servicios definidos en `docker-compose.yml`, compilando la imagen de la aplicación en el proceso.
|
||||
|
||||
## Configuración de Grafana
|
||||
|
||||
En esta sección, usaremos Prometheus como fuente de datos para mostrar métricas en gráficos de Grafana.
|
||||
|
||||
1. Navega a `http://localhost:3001` para ver la página de inicio de sesión de Grafana y utiliza `admin` tanto para el nombre de usuario como para la contraseña. Luego, requerirá agregar una nueva contraseña, y puedes mantenerla igual ya que estamos probando localmente.
|
||||
|
||||
2. Después de iniciar sesión correctamente, deberías ver el panel predeterminado de Grafana. Luego selecciona **Data Sources** en la página.
|
||||
|
||||

|
||||
|
||||
3. A continuación, selecciona Prometheus como fuente de datos:
|
||||
|
||||

|
||||
|
||||
4. Luego requerirá la URL en la que se está ejecutando el servicio de Prometheus, que será el nombre del servicio de Docker que creamos `http://prometheus:9090`.
|
||||
|
||||

|
||||
|
||||
5. Finalmente, haz clic en el botón **Save & Test** para verificar la conexión de la fuente de datos.
|
||||
|
||||

|
||||
|
||||
¡Genial! Ahora nuestro Grafana está listo para ilustrar las métricas que provienen de Prometheus.
|
||||
|
||||
## Creación de un nuevo dashboard en Grafana
|
||||
|
||||
1. Navega a `http://localhost:3001/dashboards` para crear un nuevo dashboard y agregar un nuevo panel. Haz clic en **New Dashboard** y luego en **Add New Panel** para la inicialización.
|
||||
|
||||

|
||||
|
||||
2. A continuación, seleccionamos código dentro del panel de consultas y escribimos `request_processing_seconds`. Podrás ver 3 tipos diferentes de sufijos con los datos de tus métricas personalizadas. Prometheus simplemente aplica diferentes tipos de cálculos a tus datos de manera predeterminada.
|
||||
|
||||
3. Selecciona una de las opciones y haz clic en **Run Query** para verla en el gráfico.
|
||||
|
||||

|
||||
|
||||
Finalmente, podemos ver las métricas de nuestro proyecto ilustradas muy bien por Grafana.
|
||||
|
||||
## Otras métricas
|
||||
|
||||
Hay muchos tipos de métricas disponibles según lo que requiera el proyecto. Si queremos contar un evento específico, como actualizaciones de registros en la base de datos, podemos usar `Counter()`.
|
||||
|
||||
Si tenemos una cola de mensajes como Kafka o RabbitMQ, podemos usar `Gauge()` para ilustrar el número de elementos que esperan en la cola.
|
||||
|
||||
Intenta agregar otra métrica en `main.py` como se muestra a continuación y aplica los mismos pasos para conectar Prometheus con Grafana:
|
||||
|
||||
### Ejemplo de una nueva métrica con Counter
|
||||
|
||||
```python
|
||||
UPDATE_COUNT = Counter('update_count', 'Number of updates')
|
||||
|
||||
# Dentro de tu función de procesamiento
|
||||
update_increment = random.randint(1, 100)
|
||||
UPDATE_COUNT.inc(update_increment)
|
||||
```
|
||||
|
||||
No olvides construir nuevamente la imagen de Docker para todos los servicios:
|
||||
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
42
catch-all/05_infra_test/05_prometheus_grafana/app/main.py
Normal file
42
catch-all/05_infra_test/05_prometheus_grafana/app/main.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from prometheus_client import start_http_server, Summary, Counter, Gauge, Histogram
|
||||
import random
|
||||
import time
|
||||
|
||||
# Create metrics to track time spent, requests made, and other events.
|
||||
REQUEST_TIME = Summary('request_processing_seconds',
|
||||
'Time spent processing request')
|
||||
UPDATE_COUNT = Counter('update_count', 'Number of updates')
|
||||
ACTIVE_REQUESTS = Gauge('active_requests', 'Number of active requests')
|
||||
REQUEST_LATENCY = Histogram(
|
||||
'request_latency_seconds', 'Request latency in seconds')
|
||||
|
||||
# Decorate function with metrics.
|
||||
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
@REQUEST_LATENCY.time()
|
||||
def process_request(t):
|
||||
"""A dummy function that takes some time."""
|
||||
ACTIVE_REQUESTS.inc()
|
||||
time.sleep(t)
|
||||
ACTIVE_REQUESTS.dec()
|
||||
|
||||
|
||||
def main():
|
||||
# Start up the server to expose the metrics.
|
||||
start_http_server(8000)
|
||||
print("[*] Starting server on port 8000...")
|
||||
|
||||
# Generate some requests.
|
||||
while True:
|
||||
msg = random.random()
|
||||
process_request(msg)
|
||||
update_increment = random.randint(1, 100)
|
||||
UPDATE_COUNT.inc(update_increment)
|
||||
print(
|
||||
f'[+] Processing request: {msg:.4f} | Updates: {update_increment}')
|
||||
time.sleep(random.uniform(0.5, 2.0)) # Random delay between requests
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
services:
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ${PWD}/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:9090/-/healthy"]
|
||||
interval: 1m30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
grafana:
|
||||
hostname: grafana
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
ports:
|
||||
- 3001:3000
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 1m30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
- prometheus
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- monitoring-net
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
command: ["python3", "app/main.py"]
|
||||
|
||||
networks:
|
||||
monitoring-net:
|
||||
driver: bridge
|
||||
10
catch-all/05_infra_test/05_prometheus_grafana/prometheus.yml
Normal file
10
catch-all/05_infra_test/05_prometheus_grafana/prometheus.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
global:
|
||||
scrape_interval: 15s # when Prometheus is pulling data from exporters etc
|
||||
evaluation_interval: 30s # time between each evaluation of Prometheus' alerting rules
|
||||
|
||||
scrape_configs:
|
||||
- job_name: app # your project name
|
||||
static_configs:
|
||||
- targets:
|
||||
- app:8000
|
||||
@@ -0,0 +1 @@
|
||||
prometheus-client
|
||||
203
catch-all/05_infra_test/06_sonarqube/README.md
Normal file
203
catch-all/05_infra_test/06_sonarqube/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
|
||||
# SonarQube Python Analysis con Docker
|
||||
|
||||
Este tutorial te guía a través de la configuración y ejecución de un análisis de calidad de código de un proyecto Python utilizando SonarQube y Docker Compose.
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- Docker y Docker Compose instalados en tu máquina.
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
Asegúrate de que tu directorio de proyecto tenga la siguiente estructura:
|
||||
|
||||
```
|
||||
my-python-project/
|
||||
│
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ └── utils.py
|
||||
│
|
||||
├── sonar-project.properties
|
||||
├── docker-compose.yaml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Paso 1: Crear un Proyecto Python de Ejemplo
|
||||
|
||||
Dentro de la carpeta `app`, crea los siguientes archivos:
|
||||
|
||||
### `main.py`
|
||||
|
||||
```python
|
||||
def greet(name):
|
||||
return f"Hola, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(greet("mundo"))
|
||||
```
|
||||
|
||||
### `utils.py`
|
||||
|
||||
```python
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(a, b):
|
||||
return a - b
|
||||
```
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
Este archivo puede estar vacío, simplemente indica que `app` es un paquete Python.
|
||||
|
||||
## Paso 2: Configurar SonarQube y SonarScanner
|
||||
|
||||
### 2.1. Archivo `sonar-project.properties`
|
||||
|
||||
En la raíz de tu proyecto, crea un archivo llamado `sonar-project.properties` con el siguiente contenido:
|
||||
|
||||
```properties
|
||||
sonar.projectKey=my-python-project
|
||||
sonar.projectName=My Python Project
|
||||
sonar.projectVersion=1.0
|
||||
sonar.sources=./app
|
||||
sonar.language=py
|
||||
sonar.python.version=3.8
|
||||
sonar.host.url=http://sonarqube:9000
|
||||
sonar.login=admin
|
||||
sonar.password=admin
|
||||
```
|
||||
|
||||
Asegúrate de reemplazar `my-python-project`, `My Python Project`, y la ruta del código fuente según corresponda.
|
||||
|
||||
## Paso 3: Crear `docker-compose.yaml`
|
||||
|
||||
Crea un archivo `docker-compose.yaml` en la raíz de tu proyecto con el siguiente contenido:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
sonarqube:
|
||||
image: sonarqube:lts-community
|
||||
container_name: sonarqube
|
||||
ports:
|
||||
- "9001:9000"
|
||||
environment:
|
||||
- SONARQUBE_JDBC_URL=jdbc:postgresql://sonarqube-db:5432/sonar
|
||||
- SONARQUBE_JDBC_USERNAME=sonar
|
||||
- SONARQUBE_JDBC_PASSWORD=sonar
|
||||
- SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true
|
||||
volumes:
|
||||
- sonarqube_data:/opt/sonarqube/data
|
||||
- sonarqube_logs:/opt/sonarqube/logs
|
||||
- sonarqube_extensions:/opt/sonarqube/extensions
|
||||
networks:
|
||||
- sonarnet
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
- sonarqube-db
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- ES_JAVA_OPTS=-Xms512m -Xmx512m
|
||||
volumes:
|
||||
- elasticsearch_data:/usr/share/elasticsearch/data
|
||||
networks:
|
||||
- sonarnet
|
||||
|
||||
sonarqube-db:
|
||||
image: postgres:16.3-alpine3.20
|
||||
container_name: sonarqube-db
|
||||
environment:
|
||||
- POSTGRES_USER=sonar
|
||||
- POSTGRES_PASSWORD=sonar
|
||||
- POSTGRES_DB=sonar
|
||||
networks:
|
||||
- sonarnet
|
||||
volumes:
|
||||
- sonar_db:/var/lib/postgresql
|
||||
- sonar_db_data:/var/lib/postgresql/data
|
||||
|
||||
sonarscanner:
|
||||
image: sonarsource/sonar-scanner-cli
|
||||
container_name: sonarscanner
|
||||
depends_on:
|
||||
- sonarqube
|
||||
volumes:
|
||||
- .:/usr/src
|
||||
working_dir: /usr/src
|
||||
networks:
|
||||
- sonarnet
|
||||
entrypoint: ["sonar-scanner"]
|
||||
|
||||
networks:
|
||||
sonarnet:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
sonarqube_data:
|
||||
sonarqube_logs:
|
||||
sonarqube_extensions:
|
||||
elasticsearch_data:
|
||||
sonar_db:
|
||||
sonar_db_data:
|
||||
```
|
||||
|
||||
### Explicación
|
||||
|
||||
- **SonarQube**: Configurado para depender de Elasticsearch y PostgreSQL. Usa el contenedor `sonarqube:lts-community` que incluye SonarQube.
|
||||
- **Elasticsearch**: Usa una imagen oficial de Elasticsearch (versión 7.10.2) adecuada para la versión de SonarQube que estás utilizando. Configurado para funcionar en modo de nodo único (`discovery.type=single-node`).
|
||||
- **PostgreSQL**: Configurado para servir como la base de datos para SonarQube.
|
||||
- **SonarScanner**: Configurado para ejecutar el análisis de código.
|
||||
|
||||
## Paso 4: Ejecutar el Análisis
|
||||
|
||||
1. **Inicia SonarQube, Elasticsearch y PostgreSQL**:
|
||||
|
||||
En el directorio raíz de tu proyecto, ejecuta el siguiente comando para iniciar los servicios:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Esto levantará los contenedores de SonarQube, Elasticsearch y PostgreSQL. Dale unos minutos para que se inicien completamente.
|
||||
|
||||
2. **Ejecuta el Análisis**:
|
||||
|
||||
Una vez que SonarQube, Elasticsearch y PostgreSQL estén listos, ejecuta el siguiente comando para iniciar el análisis del código:
|
||||
|
||||
```bash
|
||||
docker-compose run sonarscanner
|
||||
```
|
||||
|
||||
## Paso 5: Ver los Resultados
|
||||
|
||||
Accede a la interfaz de SonarQube en [http://localhost:9000](http://localhost:9000). Inicia sesión con las credenciales predeterminadas:
|
||||
|
||||
- Usuario: `admin`
|
||||
- Contraseña: `admin`
|
||||
|
||||
Aquí podrás ver los resultados del análisis de calidad de tu código, incluyendo cualquier vulnerabilidad o deuda técnica identificada.
|
||||
|
||||
## Paso 6: Detener y Limpiar los Contenedores
|
||||
|
||||
Para detener y eliminar los contenedores, ejecuta:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
Esto detendrá los servicios y limpiará los recursos.
|
||||
|
||||
## Notas Finales
|
||||
|
||||
- Asegúrate de ajustar el archivo `sonar-project.properties` para que coincida con las especificaciones de tu proyecto.
|
||||
- Puedes personalizar la configuración del contenedor de SonarQube en el archivo `docker-compose.yaml` según sea necesario.
|
||||
|
||||
¡Disfruta de un análisis de código más limpio y seguro con SonarQube!
|
||||
|
||||
|
||||
6
catch-all/05_infra_test/06_sonarqube/app/main.py
Normal file
6
catch-all/05_infra_test/06_sonarqube/app/main.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def greet(name):
|
||||
return f"Hola, {name}!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(greet("Mundo"))
|
||||
6
catch-all/05_infra_test/06_sonarqube/app/utils.py
Normal file
6
catch-all/05_infra_test/06_sonarqube/app/utils.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
|
||||
def subtract(a, b):
|
||||
return a - b
|
||||
76
catch-all/05_infra_test/06_sonarqube/docker-compose.yaml
Normal file
76
catch-all/05_infra_test/06_sonarqube/docker-compose.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
services:
|
||||
sonarqube:
|
||||
image: sonarqube:lts-community
|
||||
container_name: sonarqube
|
||||
ports:
|
||||
- "9001:9000"
|
||||
environment:
|
||||
- SONARQUBE_JDBC_URL=jdbc:postgresql://sonarqube-db:5432/sonar
|
||||
- SONARQUBE_JDBC_USERNAME=sonar
|
||||
- SONARQUBE_JDBC_PASSWORD=sonar
|
||||
- SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- sonarqube_data:/opt/sonarqube/data
|
||||
- sonarqube_logs:/opt/sonarqube/logs
|
||||
- sonarqube_extensions:/opt/sonarqube/extensions
|
||||
networks:
|
||||
- sonarnet
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
- sonarqube-db
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- ES_JAVA_OPTS=-Xms512m -Xmx512m
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- elasticsearch_data:/usr/share/elasticsearch/data
|
||||
networks:
|
||||
- sonarnet
|
||||
|
||||
sonarqube-db:
|
||||
image: postgres:16.3-alpine3.20
|
||||
container_name: sonarqube-db
|
||||
environment:
|
||||
- POSTGRES_USER=sonar
|
||||
- POSTGRES_PASSWORD=sonar
|
||||
- POSTGRES_DB=sonar
|
||||
networks:
|
||||
- sonarnet
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- sonar_db:/var/lib/postgresql
|
||||
- sonar_db_data:/var/lib/postgresql/data
|
||||
|
||||
sonarscanner:
|
||||
image: sonarsource/sonar-scanner-cli
|
||||
container_name: sonarscanner
|
||||
depends_on:
|
||||
- sonarqube
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- .:/usr/src
|
||||
working_dir: /usr/src
|
||||
networks:
|
||||
- sonarnet
|
||||
entrypoint: ["sonar-scanner"]
|
||||
|
||||
networks:
|
||||
sonarnet:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
sonarqube_data:
|
||||
sonarqube_logs:
|
||||
sonarqube_extensions:
|
||||
elasticsearch_data:
|
||||
sonar_db:
|
||||
sonar_db_data:
|
||||
@@ -0,0 +1,9 @@
|
||||
sonar.projectKey=my-python-project
|
||||
sonar.projectName=My Python Project
|
||||
sonar.projectVersion=1.0
|
||||
sonar.sources=./app
|
||||
sonar.language=py
|
||||
sonar.python.version=3.8
|
||||
sonar.host.url=http://sonarqube:9000
|
||||
sonar.login=admin
|
||||
sonar.password=admin1
|
||||
16
catch-all/05_infra_test/README.md
Normal file
16
catch-all/05_infra_test/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Pruebas de despliegues en local de infraestructuras
|
||||
|
||||
<div style="display:block; margin-left:auto; margin-right:auto; width:50%;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
| Nombre | Descripción | Nivel |
|
||||
| ------------------------------------------------------- | ----------------------------------------------------- | ---------- |
|
||||
| [redis](./01_redis_flask_docker/) | Despliegue de redis y flask | Básico |
|
||||
| [rabbit](./02_rabbitmq/README.md) | Despliegue de distintas arquitecturas para rabbitmq | Intermedio |
|
||||
| [Apache Kafka](./03_kafka/README.md) | Despliegue de Apache Kafka con productor y consumidor | Intermedio |
|
||||
| [Elastic stack](./04_elastic_stack/README.md) | Despliegue de Elastic Stack | Básico |
|
||||
| [Prometheus Grafana](./05_prometheus_grafana/README.md) | Despliegue de Prometheus y Grafana para medir Python | Básico |
|
||||
| [SonarQube](./06_sonarqube/README.md) | Despliegue de SonarQube para analisis de Python | Básico |
|
||||
16
catch-all/06_bots_telegram/01_id_bot/id_bot.py
Normal file
16
catch-all/06_bots_telegram/01_id_bot/id_bot.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import telebot
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv('.env')
|
||||
BOT_TOKEN = os.getenv('BOT_TOKEN')
|
||||
bot = telebot.TeleBot(BOT_TOKEN)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['get_group_id'])
|
||||
def get_group_id(message):
|
||||
chat_id = message.chat.id
|
||||
bot.reply_to(message, f"El ID de este grupo es: {chat_id}")
|
||||
|
||||
|
||||
bot.polling()
|
||||
43
catch-all/06_bots_telegram/02_pruebas_bot/bot.py
Normal file
43
catch-all/06_bots_telegram/02_pruebas_bot/bot.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Pruebas con un bot de Telegram
|
||||
Doc: https://github.com/eternnoir/pyTelegramBotAPI
|
||||
"""
|
||||
# bot.py
|
||||
# Este es el script principal que ejecuta el bot de Telegram.
|
||||
|
||||
import signal
|
||||
from termcolor import colored
|
||||
from handlers import bot
|
||||
from logger import logger
|
||||
|
||||
|
||||
def def_handler(sig, frame):
|
||||
"""
|
||||
Manejador de señal para cerrar el programa de forma segura.
|
||||
|
||||
Args:
|
||||
sig: Señal recibida.
|
||||
frame: Frame actual.
|
||||
"""
|
||||
|
||||
print(colored(
|
||||
f"\n\n[!] Saliendo del programa...\n", "red", attrs=["bold"]
|
||||
))
|
||||
logger.info("Bot detenido por el usuario.")
|
||||
bot.stop_polling()
|
||||
exit(1)
|
||||
|
||||
|
||||
# Configurar el manejador de señal para SIGINT (Ctrl+C)
|
||||
signal.signal(signal.SIGINT, def_handler)
|
||||
|
||||
|
||||
# Iniciar el bot
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
logger.info("Bot iniciado.")
|
||||
bot.infinity_polling()
|
||||
except Exception as e:
|
||||
logger.error(f"Error en la ejecución del bot: {str(e)}")
|
||||
except KeyboardInterrupt:
|
||||
def_handler(None, None)
|
||||
19
catch-all/06_bots_telegram/02_pruebas_bot/config.py
Normal file
19
catch-all/06_bots_telegram/02_pruebas_bot/config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# config.py
|
||||
# Este módulo gestiona la configuración y la carga de variables de entorno.
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Cargar las variables de entorno desde el archivo .env
|
||||
load_dotenv('.env')
|
||||
|
||||
# Obtener el token del bot y el ID del chat de grupo desde las variables de entorno
|
||||
BOT_TOKEN = os.getenv('BOT_TOKEN')
|
||||
GROUP_CHAT_ID = os.getenv('GROUP_CHAT_ID')
|
||||
|
||||
# Validar que las variables de entorno estén configuradas
|
||||
if not BOT_TOKEN or not GROUP_CHAT_ID:
|
||||
raise AssertionError("Por favor, configura las variables de entorno BOT_TOKEN y GROUP_CHAT_ID")
|
||||
|
||||
# Convertir GROUP_CHAT_ID a entero
|
||||
GROUP_CHAT_ID = int(GROUP_CHAT_ID)
|
||||
89
catch-all/06_bots_telegram/02_pruebas_bot/handlers.py
Normal file
89
catch-all/06_bots_telegram/02_pruebas_bot/handlers.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# handlers.py
|
||||
# Este módulo contiene los manejadores de mensajes del bot.
|
||||
|
||||
from telebot import TeleBot
|
||||
from logger import logger
|
||||
import config
|
||||
|
||||
# Mensajes de texto del bot en español
|
||||
text_messages = {
|
||||
'welcome':
|
||||
u'¡Por favor denle la bienvenida a {name}!\n\n'
|
||||
u'Este chat está destinado a preguntas y discusión sobre la pyTelegramBotAPI.\n'
|
||||
u'Para permitir que los miembros del grupo respondan a tus preguntas de forma rápida y precisa, asegúrate de '
|
||||
u'estudiar la documentación del proyecto (https://github.com/eternnoir/pyTelegramBotAPI/blob/master/README.md) '
|
||||
u'y los ejemplos (https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples) primero.\n\n'
|
||||
u'¡Espero que disfrutes tu estadía aquí!',
|
||||
|
||||
'info':
|
||||
u'Mi nombre es TeleBot,\n'
|
||||
u'Soy un bot que asiste a estas maravillosas personas que crean bots en este chat del grupo de la librería.\n'
|
||||
u'Además, todavía estoy en desarrollo. ¡Por favor, mejora mi funcionalidad haciendo una solicitud de pull! '
|
||||
u'Las sugerencias también son bienvenidas, solo déjalas en este chat del grupo!',
|
||||
|
||||
'wrong_chat':
|
||||
u'¡Hola!\nGracias por probarme. Sin embargo, este bot solo puede usarse en el chat del grupo pyTelegramAPI.\n'
|
||||
u'¡Únete a nosotros!\n\n'
|
||||
u'https://telegram.me/joinchat/067e22c60035523fda8f6025ee87e30b'
|
||||
}
|
||||
|
||||
# Crear una instancia del bot con el token de configuración
|
||||
bot = TeleBot(config.BOT_TOKEN)
|
||||
|
||||
# Manejador para cuando un nuevo participante se une al chat
|
||||
@bot.message_handler(func=lambda m: True, content_types=['new_chat_participant'])
|
||||
def on_user_joins(message):
|
||||
try:
|
||||
# Obtener el nombre del nuevo participante
|
||||
name = message.new_chat_participant.first_name
|
||||
if hasattr(message.new_chat_participant, 'last_name') and message.new_chat_participant.last_name is not None:
|
||||
name += f" {message.new_chat_participant.last_name}"
|
||||
|
||||
if hasattr(message.new_chat_participant, 'username') and message.new_chat_participant.username is not None:
|
||||
name += f" (@{message.new_chat_participant.username})"
|
||||
|
||||
# Enviar mensaje de bienvenida
|
||||
bot.reply_to(message, text_messages['welcome'].format(name=name))
|
||||
except Exception as e:
|
||||
logger.error(f"Error en on_user_joins: {str(e)}")
|
||||
|
||||
# Manejador para los comandos /info y /help
|
||||
@bot.message_handler(commands=['info', 'help'])
|
||||
def on_info(message):
|
||||
try:
|
||||
bot.reply_to(message, text_messages['info'])
|
||||
except Exception as e:
|
||||
logger.error(f"Error en on_info: {str(e)}")
|
||||
|
||||
# Manejador para el comando /ping
|
||||
@bot.message_handler(commands=["ping"])
|
||||
def on_ping(message):
|
||||
try:
|
||||
bot.reply_to(message, "¡Sigo vivo y pateando!")
|
||||
except Exception as e:
|
||||
logger.error(f"Error en on_ping: {str(e)}")
|
||||
|
||||
|
||||
# Manejador para el comando /tq
|
||||
@bot.message_handler(commands=["tq"])
|
||||
def on_ping(message):
|
||||
try:
|
||||
bot.reply_to(message, "¡Te quiero princesa!")
|
||||
except Exception as e:
|
||||
logger.error(f"Error en on_tq: {str(e)}")
|
||||
|
||||
|
||||
# Manejador para el comando /start
|
||||
@bot.message_handler(commands=['start'])
|
||||
def on_start(message):
|
||||
try:
|
||||
bot.reply_to(message, "¡Bienvenido! Usa /info para más información.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error en on_start: {str(e)}")
|
||||
|
||||
# Listener para registrar todos los mensajes recibidos
|
||||
def listener(messages):
|
||||
for m in messages:
|
||||
logger.info(f"Mensaje recibido: {str(m)}")
|
||||
|
||||
bot.set_update_listener(listener)
|
||||
23
catch-all/06_bots_telegram/02_pruebas_bot/logger.py
Normal file
23
catch-all/06_bots_telegram/02_pruebas_bot/logger.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# logger.py
|
||||
# Este módulo configura el logging con rotación de archivos.
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# Ruta del archivo de log
|
||||
log_file = 'logs/bot.log'
|
||||
|
||||
# Formato de los mensajes de log
|
||||
log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Configurar RotatingFileHandler
|
||||
# maxBytes: tamaño máximo del archivo de log en bytes (5MB en este caso)
|
||||
# backupCount: número máximo de archivos de respaldo
|
||||
file_handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=5)
|
||||
file_handler.setFormatter(log_formatter)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# Configurar el logger
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.addHandler(file_handler)
|
||||
15
catch-all/06_bots_telegram/02_pruebas_bot/utils.py
Normal file
15
catch-all/06_bots_telegram/02_pruebas_bot/utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# utils.py
|
||||
# Este módulo contiene funciones utilitarias.
|
||||
|
||||
def is_api_group(chat_id, group_chat_id):
|
||||
"""
|
||||
Verifica si el chat_id proporcionado es el mismo que el ID del grupo API.
|
||||
|
||||
Args:
|
||||
chat_id (int): El ID del chat a verificar.
|
||||
group_chat_id (int): El ID del grupo API.
|
||||
|
||||
Returns:
|
||||
bool: True si chat_id es igual a group_chat_id, de lo contrario False.
|
||||
"""
|
||||
return chat_id == group_chat_id
|
||||
10
catch-all/06_bots_telegram/03_translator_bot/Dockerfile
Normal file
10
catch-all/06_bots_telegram/03_translator_bot/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
CMD ["python", "translator.py"]
|
||||
|
||||
19
catch-all/06_bots_telegram/03_translator_bot/config.py
Normal file
19
catch-all/06_bots_telegram/03_translator_bot/config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# config.py
|
||||
# Este módulo gestiona la configuración y la carga de variables de entorno.
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Cargar las variables de entorno desde el archivo .env
|
||||
load_dotenv('.env')
|
||||
|
||||
# Obtener el token del bot y el ID del chat de grupo desde las variables de entorno
|
||||
BOT_TOKEN = os.getenv('BOT_TOKEN')
|
||||
GROUP_CHAT_ID = os.getenv('GROUP_CHAT_ID')
|
||||
|
||||
# Validar que las variables de entorno estén configuradas
|
||||
if not BOT_TOKEN or not GROUP_CHAT_ID:
|
||||
raise AssertionError("Por favor, configura las variables de entorno BOT_TOKEN y GROUP_CHAT_ID")
|
||||
|
||||
# Convertir GROUP_CHAT_ID a entero
|
||||
GROUP_CHAT_ID = int(GROUP_CHAT_ID)
|
||||
63
catch-all/06_bots_telegram/03_translator_bot/logger.py
Normal file
63
catch-all/06_bots_telegram/03_translator_bot/logger.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
# Ejemplo de uso del logger
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info('Logger configurado correctamente.')
|
||||
logger.debug('Este es un mensaje de depuración.')
|
||||
logger.warning('Este es un mensaje de advertencia.')
|
||||
logger.error('Este es un mensaje de error.')
|
||||
logger.critical('Este es un mensaje crítico.')
|
||||
"""
|
||||
|
||||
# logger.py
|
||||
# Este módulo configura el logging con rotación de archivos.
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
|
||||
def setup_logger():
|
||||
# Crear el directorio de logs si no existe
|
||||
log_directory = 'logs'
|
||||
if not os.path.exists(log_directory):
|
||||
os.makedirs(log_directory)
|
||||
|
||||
# Ruta del archivo de log
|
||||
log_file = os.path.join(log_directory, 'bot.log')
|
||||
|
||||
# Formato de los mensajes de log
|
||||
log_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Configurar RotatingFileHandler
|
||||
# maxBytes: tamaño máximo del archivo de log en bytes (5MB en este caso)
|
||||
# backupCount: número máximo de archivos de respaldo
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file, maxBytes=5*1024*1024, backupCount=5)
|
||||
file_handler.setFormatter(log_formatter)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# Configurar StreamHandler para la consola
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(log_formatter)
|
||||
|
||||
# Puedes cambiar este nivel según tus necesidades
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# Configurar el logger
|
||||
logger = logging.getLogger('telegram_bot')
|
||||
# Configurar el nivel del logger a DEBUG para capturar todos los mensajes
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Evitar que los mensajes se dupliquen en el log
|
||||
logger.propagate = False
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# Inicializar el logger
|
||||
logger = setup_logger()
|
||||
@@ -0,0 +1,3 @@
|
||||
python-telegram-bot==21.3
|
||||
translate==3.6.1
|
||||
python-dotenv==1.0.1
|
||||
212
catch-all/06_bots_telegram/03_translator_bot/translator.py
Normal file
212
catch-all/06_bots_telegram/03_translator_bot/translator.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Este módulo contiene el código para un bot de Telegram que realiza tareas de traducción.
|
||||
|
||||
El bot utiliza la API de Telegram Bot para interactuar con los usuarios y la API de Google Translate para la traducción.
|
||||
|
||||
La función principal inicializa el bot y comienza a escuchar los mensajes entrantes.
|
||||
|
||||
Para ejecutar el bot, asegúrese de tener las credenciales y la configuración de la API necesarias configuradas en el módulo `config`.
|
||||
|
||||
Extraido del tutorial de youtube y luego actualizado: https://www.youtube.com/watch?v=8buZAq148gk&ab_channel=SBDeveloper
|
||||
|
||||
Autor: manuelver
|
||||
"""
|
||||
|
||||
import signal
|
||||
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import ApplicationBuilder, MessageHandler, CommandHandler, CallbackContext, filters, CallbackQueryHandler
|
||||
|
||||
from translate import Translator
|
||||
|
||||
import config
|
||||
from logger import logger
|
||||
|
||||
|
||||
def def_handler(sig, frame):
|
||||
"""
|
||||
Función manejadora de señales para salir del programa de manera elegante.
|
||||
"""
|
||||
logger.info("Saliendo del programa...")
|
||||
print("\n[!] Saliendo del programa...")
|
||||
exit(1)
|
||||
|
||||
|
||||
# Configurar el manejador de señal para SIGINT (Ctrl+C)
|
||||
signal.signal(signal.SIGINT, def_handler)
|
||||
|
||||
|
||||
async def select_origin_lang(update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Función para seleccionar el idioma de origen para la traducción.
|
||||
"""
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("Español", callback_data='es'),
|
||||
InlineKeyboardButton("Catalán", callback_data='ca'),
|
||||
InlineKeyboardButton("English", callback_data='en'),
|
||||
InlineKeyboardButton("Français", callback_data='fr')
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Deutsch", callback_data='de'),
|
||||
InlineKeyboardButton("Italiano", callback_data='it'),
|
||||
InlineKeyboardButton("Português", callback_data='pt')
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Русский (Ruso)", callback_data='ru'),
|
||||
InlineKeyboardButton("日本語 (Japonés)", callback_data='ja'),
|
||||
InlineKeyboardButton("中文 (Chino)", callback_data='zh')
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("(Árabe) العربية", callback_data='ar'),
|
||||
InlineKeyboardButton("हिन्दी (Hindi)", callback_data='hi'),
|
||||
InlineKeyboardButton("עברית (Hebreo)", callback_data='he')
|
||||
]
|
||||
]
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
await update.message.reply_text(
|
||||
'Por favor, selecciona el idioma de origen:', reply_markup=reply_markup)
|
||||
|
||||
|
||||
async def select_dest_lang(update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Función para seleccionar el idioma de destino para la traducción.
|
||||
"""
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("Español", callback_data='es'),
|
||||
InlineKeyboardButton("Catalán", callback_data='ca'),
|
||||
InlineKeyboardButton("English", callback_data='en'),
|
||||
InlineKeyboardButton("Français", callback_data='fr')
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Deutsch", callback_data='de'),
|
||||
InlineKeyboardButton("Italiano", callback_data='it'),
|
||||
InlineKeyboardButton("Português", callback_data='pt')
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Русский (Ruso)", callback_data='ru'),
|
||||
InlineKeyboardButton("日本語 (Japonés)", callback_data='ja'),
|
||||
InlineKeyboardButton("中文 (Chino)", callback_data='zh')
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("(Árabe) العربية", callback_data='ar'),
|
||||
InlineKeyboardButton("हिन्दी (Hindi)", callback_data='hi'),
|
||||
InlineKeyboardButton("עברית (Hebreo)", callback_data='he')
|
||||
]
|
||||
]
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
await update.message.reply_text(
|
||||
f'Por favor, selecciona el idioma de destino:', reply_markup=reply_markup)
|
||||
|
||||
|
||||
|
||||
async def button(update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Función para manejar los botones de idioma seleccionados.
|
||||
"""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
if 'origin_lang' not in context.user_data:
|
||||
context.user_data['origin_lang'] = query.data
|
||||
await query.edit_message_text(text=f"Idioma de origen seleccionado.\n\nAhora selecciona el idioma de destino con el comando /langTo")
|
||||
await select_dest_lang(update, context)
|
||||
else:
|
||||
context.user_data['dest_lang'] = query.data
|
||||
await query.edit_message_text(text=f"Idioma de destino seleccionado.\n\nEnvía tu texto para traducir.")
|
||||
|
||||
logger.info(f"Idioma seleccionado: {query.data}")
|
||||
|
||||
|
||||
async def lang_translator(user_input, from_lang, to_lang):
|
||||
|
||||
try:
|
||||
translator = Translator(from_lang=from_lang, to_lang=to_lang)
|
||||
translation = translator.translate(user_input)
|
||||
|
||||
return translation
|
||||
|
||||
except TranslationError as e:
|
||||
logger.error(f"Error en la traducción: {str(e)}")
|
||||
return "Error en la traducción. Por favor, intenta de nuevo más tarde."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado al traducir el texto: {str(e)}")
|
||||
return "Error inesperado al traducir el texto."
|
||||
|
||||
|
||||
async def reply(update: Update, context: CallbackContext):
|
||||
|
||||
user_input = update.message.text
|
||||
from_lang = context.user_data.get(
|
||||
'origin_lang', 'es') # Español por defecto
|
||||
to_lang = context.user_data.get('dest_lang', 'en') # Inglés por defecto
|
||||
|
||||
translation = await lang_translator(user_input, from_lang, to_lang)
|
||||
await update.message.reply_text(translation)
|
||||
|
||||
logger.info(f"Mensaje recibido: {user_input}")
|
||||
logger.info(f"Texto traducido: {translation}")
|
||||
|
||||
|
||||
async def start(update: Update, context: CallbackContext):
|
||||
|
||||
await update.message.reply_text("¡Hola! Soy un bot de traducción.\n\nSi necesitas ayuda: /help.")
|
||||
|
||||
logger.info("Comando /start recibido.")
|
||||
|
||||
|
||||
async def help_command(update: Update, context: CallbackContext):
|
||||
"""
|
||||
Función para mostrar los comandos disponibles.
|
||||
"""
|
||||
commands = [
|
||||
"/start - Iniciar el bot",
|
||||
"/langFrom - Seleccionar idioma de origen",
|
||||
"/langTo - Seleccionar idioma de destino",
|
||||
"/help - Mostrar este mensaje de ayuda"
|
||||
]
|
||||
help_text = "\n".join(commands)
|
||||
|
||||
await update.message.reply_text(f"El idioma por defecto origen es español y el de destino el Inglés.\nPuedes configurar otras opciones.\nUna vez lo tengas listo tan solo tienes que enviar el texto con el idioma origen.\n\nOpciones:\n{help_text}")
|
||||
|
||||
logger.info("Comando /help recibido.")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Función principal para inicializar el bot y comenzar a escuchar los mensajes.
|
||||
"""
|
||||
api = config.BOT_TOKEN
|
||||
|
||||
application = ApplicationBuilder().token(api).build()
|
||||
|
||||
# Manejadores de comandos y mensajes
|
||||
application.add_handler(CommandHandler('start', start))
|
||||
application.add_handler(CommandHandler('langFrom', select_origin_lang))
|
||||
application.add_handler(CommandHandler('langTo', select_dest_lang))
|
||||
application.add_handler(CommandHandler('help', help_command))
|
||||
application.add_handler(CommandHandler('command', help_command))
|
||||
application.add_handler(MessageHandler(
|
||||
filters.TEXT & ~filters.COMMAND, reply))
|
||||
application.add_handler(CallbackQueryHandler(button))
|
||||
|
||||
# Iniciar el bot
|
||||
logger.info("Bot iniciado.")
|
||||
|
||||
# Iniciar el bucle de eventos
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
logger.error(f'Error en la ejecución del bot: {str(e)}')
|
||||
except KeyboardInterrupt:
|
||||
def_handler(None, None)
|
||||
10
catch-all/06_bots_telegram/04_clima_bot/Dockerfile
Normal file
10
catch-all/06_bots_telegram/04_clima_bot/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
CMD ["python", "clima_bot.py"]
|
||||
|
||||
126
catch-all/06_bots_telegram/04_clima_bot/clima_bot.py
Normal file
126
catch-all/06_bots_telegram/04_clima_bot/clima_bot.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, CallbackQueryHandler, CallbackContext
|
||||
from telegram.ext import filters
|
||||
|
||||
import config
|
||||
|
||||
# Logging setup
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TELEGRAM_API_TOKEN = config.BOT_TOKEN
|
||||
WEATHER_API_KEY = config.WEATHER_API_KEY
|
||||
BASE_WEATHER_URL = "http://api.openweathermap.org/data/2.5/weather?q={}&appid={}"
|
||||
FORECAST_URL = "http://api.openweathermap.org/data/2.5/forecast?q={}&appid={}"
|
||||
|
||||
city = None
|
||||
|
||||
|
||||
def weather_emoji(description):
|
||||
"""Devuelve un emoji basado en la descripción del clima."""
|
||||
description = description.lower()
|
||||
if "clear" in description:
|
||||
return "☀️"
|
||||
elif "cloud" in description:
|
||||
return "☁️"
|
||||
elif "rain" in description:
|
||||
return "🌧️"
|
||||
elif "thunder" in description:
|
||||
return "⛈️"
|
||||
elif "snow" in description:
|
||||
return "❄️"
|
||||
elif "mist" in description or "fog" in description:
|
||||
return "🌫️"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
async def start(update: Update, context: CallbackContext) -> None:
|
||||
"""Manejador del comando /start. Solicita al usuario la ciudad en la que vive."""
|
||||
logger.debug(f"Comando /start recibido de {update.message.chat.username}")
|
||||
await update.message.reply_text("¿En qué ciudad vives?")
|
||||
|
||||
|
||||
async def menu(update: Update, context: CallbackContext) -> None:
|
||||
"""Manejador de mensajes. Muestra un menú con opciones al usuario después de recibir la ciudad."""
|
||||
global city
|
||||
city = update.message.text
|
||||
logger.debug(f"Ciudad recibida: {city}")
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("Clima Actual", callback_data='current_weather')],
|
||||
[InlineKeyboardButton("Pronóstico del Tiempo", callback_data='forecast')],
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await update.message.reply_text('Elige una opción:', reply_markup=reply_markup)
|
||||
|
||||
|
||||
async def button(update: Update, context: CallbackContext) -> None:
|
||||
"""Manejador de botones. Procesa la opción seleccionada por el usuario."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
logger.debug(f"Opción seleccionada: {query.data}")
|
||||
|
||||
try:
|
||||
if query.data == 'current_weather':
|
||||
response = requests.get(BASE_WEATHER_URL.format(city, WEATHER_API_KEY))
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
main = data['main']
|
||||
weather_data = data['weather'][0]
|
||||
celsius_temp = main['temp'] - 273.15
|
||||
emoji = weather_emoji(weather_data['description'])
|
||||
message = f"Clima actual en {city} {emoji}:\n"
|
||||
message += f"Temperatura: {celsius_temp:.2f}°C\n"
|
||||
message += f"Descripción: {weather_data['description'].capitalize()}\n"
|
||||
message += f"Humedad: {main['humidity']}%\n"
|
||||
await query.edit_message_text(text=message)
|
||||
elif query.data == 'forecast':
|
||||
response = requests.get(FORECAST_URL.format(city, WEATHER_API_KEY))
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
message = f"Pronóstico del tiempo para {city}:\n"
|
||||
for item in data['list'][:5]:
|
||||
celsius_temp = item['main']['temp'] - 273.15
|
||||
emoji = weather_emoji(item['weather'][0]['description'])
|
||||
message += f"\nFecha: {item['dt_txt']} {emoji}\n"
|
||||
message += f"Temperatura: {celsius_temp:.2f}°C\n"
|
||||
message += f"Descripción: {item['weather'][0]['description'].capitalize()}\n"
|
||||
await query.edit_message_text(text=message)
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Error al obtener datos del clima: {e}")
|
||||
await query.edit_message_text(
|
||||
text="No se puede encontrar información meteorológica para esta ciudad. Inténtalo de nuevo.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado: {e}")
|
||||
await query.edit_message_text(
|
||||
text="Ocurrió un error inesperado. Inténtalo de nuevo más tarde.")
|
||||
|
||||
|
||||
def error(update: Update, context: CallbackContext):
|
||||
"""Registra errores causados por actualizaciones."""
|
||||
logger.warning('La actualización "%s" causó el error "%s"', update, context.error)
|
||||
|
||||
|
||||
def main():
|
||||
"""Función principal del bot. Configura y ejecuta el bot de Telegram."""
|
||||
logger.info("Iniciando el bot...")
|
||||
application = ApplicationBuilder().token(TELEGRAM_API_TOKEN).build()
|
||||
|
||||
application.add_handler(CommandHandler("start", start))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, menu))
|
||||
application.add_handler(CallbackQueryHandler(button))
|
||||
|
||||
# Registra todos los errores
|
||||
application.add_error_handler(error)
|
||||
|
||||
logger.info("Bot iniciado y en espera de mensajes...")
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
17
catch-all/06_bots_telegram/04_clima_bot/config.py
Normal file
17
catch-all/06_bots_telegram/04_clima_bot/config.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# config.py
|
||||
# Este módulo gestiona la configuración y la carga de variables de entorno.
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Cargar las variables de entorno desde el archivo .env
|
||||
load_dotenv('.env')
|
||||
|
||||
# Obtener el token del bot y el ID del chat de grupo desde las variables de entorno
|
||||
BOT_TOKEN = os.getenv('BOT_TOKEN')
|
||||
WEATHER_API_KEY = os.getenv('WEATHER_API_KEY')
|
||||
|
||||
# Validar que las variables de entorno estén configuradas
|
||||
if not BOT_TOKEN or not WEATHER_API_KEY:
|
||||
raise AssertionError("Por favor, configura las variables de entorno BOT_TOKEN y GROUP_CHAT_ID")
|
||||
|
||||
13
catch-all/06_bots_telegram/04_clima_bot/docker-compose.yaml
Normal file
13
catch-all/06_bots_telegram/04_clima_bot/docker-compose.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
clima-app:
|
||||
build: .
|
||||
container_name: clima-app
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pgrep -f bot.py || exit 1"]
|
||||
interval: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
13
catch-all/06_bots_telegram/04_clima_bot/requirements.txt
Normal file
13
catch-all/06_bots_telegram/04_clima_bot/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
certifi==2024.7.4
|
||||
cffi==1.16.0
|
||||
charset-normalizer==3.3.2
|
||||
cryptography==42.0.8
|
||||
decorator==5.1.1
|
||||
idna==3.7
|
||||
pycparser==2.22
|
||||
python-decouple==3.8
|
||||
python-dotenv==1.0.1
|
||||
python-telegram-bot==21.3
|
||||
requests==2.32.3
|
||||
tornado==6.4.1
|
||||
urllib3~=1.26
|
||||
11
catch-all/06_bots_telegram/05_rss_bot/Dockerfile
Normal file
11
catch-all/06_bots_telegram/05_rss_bot/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add sqlite sqlite-libs sqlite-dev && \
|
||||
pip install -r requirements.txt
|
||||
|
||||
CMD ["python", "rss2telegram.py"]
|
||||
22
catch-all/06_bots_telegram/05_rss_bot/config.py
Normal file
22
catch-all/06_bots_telegram/05_rss_bot/config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# config.py
|
||||
# Este módulo gestiona la configuración y la carga de variables de entorno.
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Cargar las variables de entorno desde el archivo .env
|
||||
load_dotenv('.env')
|
||||
|
||||
# Obtener el token del bot y el ID del chat de grupo desde las variables de entorno
|
||||
Token = os.getenv('BOT_TOKEN')
|
||||
chatid = os.getenv('GROUP_CHAT_ID')
|
||||
|
||||
# Este es el retraso entre cada sondeo a las fuentes RSS en segundos.
|
||||
delay = 30
|
||||
|
||||
# Validar que las variables de entorno estén configuradas
|
||||
if not Token or not chatid:
|
||||
raise AssertionError("Por favor, configura las variables de entorno BOT_TOKEN y GROUP_CHAT_ID")
|
||||
|
||||
# Convertir GROUP_CHAT_ID a entero
|
||||
chatid = int(chatid)
|
||||
3
catch-all/06_bots_telegram/05_rss_bot/requirements.txt
Normal file
3
catch-all/06_bots_telegram/05_rss_bot/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
python-dotenv==1.0.1
|
||||
feedparser==6.0.11
|
||||
python-telegram-bot[job-queue]==21.4
|
||||
175
catch-all/06_bots_telegram/05_rss_bot/rss2telegram.py
Normal file
175
catch-all/06_bots_telegram/05_rss_bot/rss2telegram.py
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
#-*- coding: utf-8 -*-
|
||||
import feedparser
|
||||
import logging
|
||||
import sqlite3
|
||||
from telegram import ForceReply, Update
|
||||
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
||||
from pathlib import Path
|
||||
|
||||
config = Path("./config.py")
|
||||
try:
|
||||
config.resolve(strict=True)
|
||||
except FileNotFoundError:
|
||||
print("Por favor, copia config.py.sample a config.py y rellena las propiedades.")
|
||||
exit()
|
||||
|
||||
|
||||
import config
|
||||
|
||||
rss_dict = {}
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
|
||||
|
||||
# SQLITE
|
||||
def sqlite_connect():
|
||||
global conn
|
||||
conn = sqlite3.connect('rss.db', check_same_thread=False)
|
||||
|
||||
|
||||
def sqlite_load_all():
|
||||
sqlite_connect()
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT * FROM rss')
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
def sqlite_write(name, link, last):
|
||||
sqlite_connect()
|
||||
c = conn.cursor()
|
||||
q = [(name), (link), (last)]
|
||||
c.execute('''INSERT INTO rss('name','link','last') VALUES(?,?,?)''', q)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
# RSS
|
||||
def rss_load():
|
||||
# if the dict is not empty, empty it.
|
||||
if bool(rss_dict):
|
||||
rss_dict.clear()
|
||||
|
||||
for row in sqlite_load_all():
|
||||
rss_dict[row[0]] = (row[1], row[2])
|
||||
|
||||
|
||||
async def cmd_rss_list(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if bool(rss_dict) is False:
|
||||
await update.message.reply_text("The database is empty")
|
||||
else:
|
||||
for title, url_list in rss_dict.items():
|
||||
await update.message.reply_text(
|
||||
"Título: " + title +
|
||||
"\nURL RSS: " + url_list[0] +
|
||||
"\nÚltima noticia comprobada:" + url_list[1])
|
||||
|
||||
|
||||
async def cmd_rss_add(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
# try if there are 2 arguments passed
|
||||
try:
|
||||
context.args[1]
|
||||
except IndexError:
|
||||
await update.message.reply_text("ERROR: EL formato debe ser: /add <título> <enlace>")
|
||||
raise
|
||||
# try if the url is a valid RSS feed
|
||||
try:
|
||||
rss_d = feedparser.parse(context.args[1])
|
||||
rss_d.entries[0]['title']
|
||||
except IndexError:
|
||||
await update.message.reply_text(
|
||||
"ERROR: EL enlace no parece ser un feed RSS o no es compatible")
|
||||
raise
|
||||
sqlite_write(context.args[0], context.args[1], str(rss_d.entries[0]['link']))
|
||||
rss_load()
|
||||
await update.message.reply_text("Añadido \nTÍTULO: %s\nRSS: %s" % (context.args[0], context.args[1]))
|
||||
print("Añadido \nTÍTULO: %s\nRSS: %s" % (context.args[0], context.args[1]))
|
||||
|
||||
|
||||
async def cmd_rss_remove(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
conn = sqlite3.connect('rss.db')
|
||||
c = conn.cursor()
|
||||
name = str(context.args[0])
|
||||
try:
|
||||
c.execute('DELETE FROM rss WHERE name = ?', [name])
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
print('Error %s:' % e)
|
||||
rss_load()
|
||||
await update.message.reply_text("Borrado: " + name)
|
||||
print("Borrado: " + name)
|
||||
|
||||
|
||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(
|
||||
"RSS en Telegram bot" +
|
||||
"\n\nDespués de añadir con éxito un enlace RSS, el bot comienza a buscar la fuente cada "
|
||||
+ str(config.delay) + " segundos. (Puedes configurarlo en config.py) ⏰⏰⏰" +
|
||||
"\n\nLos Títulos son usados para gestionar fácilmente los feeds RSS y deben contener solo una palabra 📝📝📝" +
|
||||
"\n\nComandos:" +
|
||||
"\n/help Muestra este mensaje de ayuda." +
|
||||
"\n/add <title> <link> Para añadir una RSS en la base de datos." +
|
||||
"\n/remove <title> Borra una RSS de la base de datos."
|
||||
"\n/list Listar todos los títulos y RSS guardados.")
|
||||
|
||||
|
||||
async def rss_monitor(context: ContextTypes.DEFAULT_TYPE):
|
||||
for name, url_list in rss_dict.items():
|
||||
rss_d = feedparser.parse(url_list[0])
|
||||
if (url_list[1] != rss_d.entries[0]['link']):
|
||||
print("Nueva RSS para " + name + ", actualizando base de datos...")
|
||||
conn = sqlite3.connect('rss.db')
|
||||
q = [(str(rss_d.entries[0]['link'])), (name)]
|
||||
c = conn.cursor()
|
||||
c.execute('''UPDATE rss SET 'last' = ? WHERE name=? ''', q)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
rss_load()
|
||||
print("Emviando RSS a Telegram...")
|
||||
await context.bot.send_message(config.chatid, rss_d.entries[0]['link'])
|
||||
print("Éxito.")
|
||||
|
||||
|
||||
def init_sqlite():
|
||||
conn = sqlite3.connect('rss.db')
|
||||
c = conn.cursor()
|
||||
c.execute('''CREATE TABLE rss (name text, link text, last text)''')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dp = Application.builder().token(config.Token).build()
|
||||
|
||||
dp.add_handler(CommandHandler("add", cmd_rss_add))
|
||||
dp.add_handler(CommandHandler("help", cmd_help))
|
||||
dp.add_handler(CommandHandler("start", cmd_help))
|
||||
dp.add_handler(CommandHandler("list", cmd_rss_list))
|
||||
dp.add_handler(CommandHandler("remove", cmd_rss_remove))
|
||||
|
||||
#dp.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
||||
|
||||
|
||||
db = Path("./rss.db")
|
||||
try:
|
||||
db.resolve(strict=True)
|
||||
except FileNotFoundError:
|
||||
print("Base de datos no encontrada, intenta crear una nueva.")
|
||||
try:
|
||||
init_sqlite()
|
||||
except Exception as e:
|
||||
print("Error cuando se creaba la base de datos : ", e.message, e.args)
|
||||
pass
|
||||
else:
|
||||
print("Éxito.")
|
||||
|
||||
rss_load()
|
||||
print("Corriendo RSS Monitor.")
|
||||
|
||||
dp.job_queue.run_repeating(rss_monitor, config.delay)
|
||||
dp.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
26
catch-all/06_bots_telegram/06_movie_bot/Dockerfile
Normal file
26
catch-all/06_bots_telegram/06_movie_bot/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:alpine
|
||||
|
||||
ADD requirements.txt /app/requirements.txt
|
||||
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add --no-cache bash && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
set -ex && \
|
||||
python -m venv /env && \
|
||||
/env/bin/pip install --upgrade pip && \
|
||||
/env/bin/pip install --no-cache-dir -r /app/requirements.txt && \
|
||||
runDeps="$(scanelf --needed --nobanner --recursive /env \
|
||||
| awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
|
||||
| sort -u \
|
||||
| xargs -r apk info --installed \
|
||||
| sort -u)" && \
|
||||
apk add --virtual rundeps $runDeps && \
|
||||
apk del rundeps
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
ENV VIRTUAL_ENV=/env
|
||||
ENV PATH=/env/bin:$PATH
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
42
catch-all/06_bots_telegram/06_movie_bot/docker-compose.yaml
Normal file
42
catch-all/06_bots_telegram/06_movie_bot/docker-compose.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
movie_bot:
|
||||
env_file:
|
||||
- .env
|
||||
image: movie_bot_python:latest
|
||||
container_name: movie_bot
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Madrid
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
depends_on:
|
||||
bbdd:
|
||||
condition: service_healthy
|
||||
|
||||
bbdd:
|
||||
image: mysql:latest
|
||||
container_name: bbdd_movie_bot
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE}
|
||||
- MYSQL_USER=${MYSQL_USER}
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
ports:
|
||||
- "3306:3306"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
46
catch-all/06_bots_telegram/06_movie_bot/main.py
Normal file
46
catch-all/06_bots_telegram/06_movie_bot/main.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from telegram.ext import CommandHandler, MessageHandler, filters, CallbackQueryHandler
|
||||
|
||||
from src import Botz
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
print(r"""
|
||||
|
||||
_
|
||||
_ __ ___ __ _ _ __ _ _ ___| |_ _____ _ __
|
||||
| '_ ` _ \ / _` | '_ \| | | |/ _ \ \ \ / / _ \ '__|
|
||||
| | | | | | (_| | | | | |_| | __/ |\ V / __/ |
|
||||
|_| |_| |_|\__,_|_| |_|\__,_|\___|_| \_/ \___|_| """)
|
||||
|
||||
bot = Botz()
|
||||
|
||||
bot.app.add_handler(CommandHandler('start', bot.start_command))
|
||||
|
||||
bot.app.add_handler(CommandHandler('help', bot.help_command))
|
||||
|
||||
bot.app.add_handler(CommandHandler("find", bot.find_title))
|
||||
|
||||
bot.app.add_handler(CommandHandler("save", bot.movie_saver))
|
||||
|
||||
bot.app.add_handler(CommandHandler("remove", bot.movie_remover))
|
||||
|
||||
bot.app.add_handler(CommandHandler("list", bot.movie_list))
|
||||
|
||||
bot.app.add_handler(CommandHandler("reboot", bot.reboot))
|
||||
|
||||
bot.app.add_handler(CommandHandler("status", bot.status))
|
||||
|
||||
bot.app.add_handler(MessageHandler(filters.TEXT, bot.any_text))
|
||||
|
||||
bot.app.add_error_handler(bot.error)
|
||||
|
||||
bot.app.add_handler(CallbackQueryHandler(bot.query_handler))
|
||||
|
||||
print('Bot Started Polling! Check Terminal for Errors')
|
||||
|
||||
bot.app.run_polling(poll_interval=3)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
20
catch-all/06_bots_telegram/06_movie_bot/requirements.txt
Normal file
20
catch-all/06_bots_telegram/06_movie_bot/requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
aiohttp==3.9.5
|
||||
aiosignal==1.3.1
|
||||
anyio==4.4.0
|
||||
async-timeout==4.0.3
|
||||
attrs==23.2.0
|
||||
certifi==2024.7.4
|
||||
charset-normalizer==3.3.2
|
||||
frozenlist==1.4.1
|
||||
h11==0.14.0
|
||||
httpcore==1.0.5
|
||||
httpx==0.27.0
|
||||
idna==3.7
|
||||
multidict==6.0.5
|
||||
mysql-connector-python==9.0.0
|
||||
protobuf==5.27.2
|
||||
python-dotenv==1.0.1
|
||||
python-telegram-bot==21.4
|
||||
sniffio==1.3.1
|
||||
tcp-latency==0.0.12
|
||||
yarl==1.9.4
|
||||
1
catch-all/06_bots_telegram/06_movie_bot/src/__init__.py
Normal file
1
catch-all/06_bots_telegram/06_movie_bot/src/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .bot import Botz
|
||||
468
catch-all/06_bots_telegram/06_movie_bot/src/bot.py
Normal file
468
catch-all/06_bots_telegram/06_movie_bot/src/bot.py
Normal file
@@ -0,0 +1,468 @@
|
||||
from typing import Final
|
||||
from os import getenv, execl
|
||||
import sys
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import Application, ContextTypes
|
||||
import aiohttp
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
import mysql.connector as msc
|
||||
import time
|
||||
from tcp_latency import measure_latency
|
||||
|
||||
print('Iniciando bot...')
|
||||
|
||||
load_dotenv('.env')
|
||||
|
||||
# Telegram Bot
|
||||
BOT_USERNAME: Final = getenv("BOT_USERNAME")
|
||||
BOT_API: Final = getenv("BOT_API")
|
||||
|
||||
# OMDB
|
||||
OMDB: Final = "http://www.omdbapi.com"
|
||||
OMDB_SITE: Final = "www.omdbapi.com"
|
||||
OMDB_API: Final = getenv("OMDB_API")
|
||||
|
||||
# TMDB
|
||||
TMDB_API: Final = getenv("TMDB_API")
|
||||
TMDB_SITE: Final = "api.themoviedb.org"
|
||||
|
||||
# IMDB
|
||||
IMDB_LINK: Final = "https://www.imdb.com/title/"
|
||||
|
||||
# MySQL
|
||||
MYSQL_HOST: Final = getenv("MYSQL_HOST")
|
||||
MYSQL_PORT: Final = 3306
|
||||
MYSQL_USER: Final = getenv("MYSQL_USER")
|
||||
MYSQL_PASSWORD: Final = getenv("MYSQL_PASSWORD")
|
||||
MYSQL_DATABASE: Final = getenv("MYSQL_DATABASE")
|
||||
CREATE_TABLE: Final = getenv("CREATE_TABLE", True)
|
||||
|
||||
|
||||
class Botz:
|
||||
|
||||
INTRO_MSG = "*¡Hola! Soy un Bot de Películas \n" \
|
||||
"Escribe /help para ver la lista de comandos disponibles* \n"
|
||||
|
||||
HELP_MSG = "*COMANDOS DISPONIBLES*\n\n" \
|
||||
" * Comando :* /start\n" \
|
||||
" * Descripción :* Muestra la introducción.\n\n" \
|
||||
" * Comando :* /help\n" \
|
||||
" * Descripción :* Lista los comandos disponibles.\n\n" \
|
||||
" * Comando :* /status\n" \
|
||||
" * Descripción :* Devuelve el estado del bot.\n\n" \
|
||||
" * Comando :* /find nombre-de-la-pelicula \n" \
|
||||
" * Descripción :* " \
|
||||
" Proporciona los detalles de la película/serie especificada." \
|
||||
" Introduce el nombre de la película como argumento del comando /find." \
|
||||
" Usa los botones debajo para obtener más información. \n" \
|
||||
" ej: /find Godfather \n\n" \
|
||||
" * Comando :* /find nombre-de-la-pelicula y=año\n" \
|
||||
" * Descripción :* " \
|
||||
" Proporciona los detalles de la película/serie especificada." \
|
||||
" Introduce el nombre de la película y el año como argumento del comando /find." \
|
||||
" Usa los botones de debajo para obtener más información. \n" \
|
||||
" ej: /find Godfather y=1972\n\n" \
|
||||
" * Comando :* /save IMDB-id \n" \
|
||||
" * Descripción :* " \
|
||||
" Introduce el ID de IMDB de la película/serie como argumento." \
|
||||
" Usa el comando /find para encontrar el ID de IMDB de una película/serie." \
|
||||
" Guarda el mensaje/archivo respondido en la base de datos con el ID de IMDB dado." \
|
||||
" Siempre usa este comando como respuesta al archivo que se guardará.\n " \
|
||||
" ej: /save tt1477834\n\n" \
|
||||
" * Comando :* /remove IMDB-id \n" \
|
||||
" * Descripción :* " \
|
||||
" Introduce el ID de IMDB de la película/serie como argumento." \
|
||||
" Usa el comando /find para encontrar el ID de IMDB de una película/serie." \
|
||||
" Elimina el archivo del ID de IMDB especificado de la base de datos.\n" \
|
||||
" ej: /remove tt1477834\n\n" \
|
||||
" * Comando :* /list \n" \
|
||||
" * Descripción :* Devuelve el número de películas/series actualmente indexadas en la base de datos.\n" \
|
||||
|
||||
MOVIE_NOT_FOUND_MSG = "*{} no está indexada actualmente en mi base de datos. 😔*\n\n" \
|
||||
"Si tienes esta película en tu chat o en otros grupos, \n" \
|
||||
" - Reenvía esa película a mi chat o a este grupo y, \n" \
|
||||
" - Usa '*/save {}*' como respuesta al archivo de la película para guardarla en mi base de datos. \n"
|
||||
|
||||
REBOOT_WAIT_MESSAGE = "Reiniciando el bot. Por favor espera ⏲️"
|
||||
|
||||
REBOOT_SUCCESS_MESSAGE = "El bot ha vuelto 😃"
|
||||
|
||||
STATUS_MESSAGE = "*El bot está vivo.* 😃 \n\n" \
|
||||
"*Estado de la base de datos :* {} \n" \
|
||||
"*Latencia de la base de datos :* {} \n" \
|
||||
"*Películas disponibles :* {} \n\n" \
|
||||
"*OMDB :* {} \n" \
|
||||
"*Latencia de OMDB :* {} \n\n" \
|
||||
"*TMDB :* {} \n" \
|
||||
"*Latencia de TMDB :* {} \n"
|
||||
|
||||
MOVIE_NOT_FOUND = "*Película/Serie NO ENCONTRADA. \n" \
|
||||
"Revisa la ortografía.* \n"
|
||||
|
||||
FIND_MSG = "*Introduce el NOMBRE de la Película/Serie junto con /find. \n" \
|
||||
"Ve a /help para más detalles.* \n"
|
||||
|
||||
INVALID_FIND_MSG = "*Comando desconocido: {}* \n" \
|
||||
"*Para buscar una película usa: /find <nombre_de_la_pelicula>* \n"
|
||||
|
||||
SAVE_MSG = "*Introduce el ID de IMDB de la Película/Serie junto con /save. \n" \
|
||||
"Ve a /help para más detalles.* \n"
|
||||
|
||||
SAVE_REPLY_MSG = "*Usa este comando como respuesta al archivo que se guardará. \n" \
|
||||
"Ve a /help para más detalles.* \n"
|
||||
|
||||
REMOVE_MSG = "*Introduce el ID de IMDB de la Película/Serie junto con /remove. \n" \
|
||||
"Ve a /help para más detalles.* \n"
|
||||
|
||||
CHECK_STATUS_MSG = "Recopilando datos. Por favor espera ⏲️"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.app = Application.builder().token(BOT_API).build()
|
||||
|
||||
# Set up bot memory
|
||||
self.memory: list = []
|
||||
|
||||
# Set up bot movie file cache memory
|
||||
self.movie_memory: list = []
|
||||
|
||||
# Set up Mysql Database
|
||||
self.connection = msc.connect(
|
||||
host=MYSQL_HOST, user=MYSQL_USER, passwd=MYSQL_PASSWORD, database=MYSQL_DATABASE)
|
||||
self.cursor = self.connection.cursor()
|
||||
if CREATE_TABLE == 'True':
|
||||
self.cursor.execute(
|
||||
"Create table movie_data(imdb_id varchar(20),from_chat_id varchar(20),message_id varchar(20))")
|
||||
|
||||
async def reboot(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
msg = await update.message.reply_text(self.REBOOT_WAIT_MESSAGE)
|
||||
time.sleep(5)
|
||||
await msg.edit_text(self.REBOOT_SUCCESS_MESSAGE)
|
||||
execl(sys.executable, f'"{sys.executable}"', *sys.argv)
|
||||
|
||||
async def status(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
message = await update.message.reply_text(self.CHECK_STATUS_MSG)
|
||||
if self.connection.is_connected():
|
||||
db_status = "Connected ✅"
|
||||
db_latency = str(round(measure_latency(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT, timeout=2.5)[0])) + "ms ⏱️"
|
||||
self.cursor.execute("select count(*) from movie_data")
|
||||
movie_number = str(self.cursor.fetchone()[0]) + ' 🎬'
|
||||
else:
|
||||
db_status = "Desconectado ❌"
|
||||
db_latency = "N/A ❌"
|
||||
movie_number = "N/A ❌"
|
||||
|
||||
omdb_params = {
|
||||
"apikey": OMDB_API,
|
||||
"t": '2012',
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(OMDB, params=omdb_params) as response:
|
||||
movie_data = await response.json()
|
||||
if movie_data["Response"] != "False":
|
||||
omdb_status = "API Disponible.✅"
|
||||
omdb_latency = str(round(measure_latency(
|
||||
host=OMDB_SITE, timeout=2.5)[0])) + "ms ⏱️"
|
||||
|
||||
find_TMDB = f'https://api.themoviedb.org/3/find/{movie_data["imdbID"]}?api_key={TMDB_API}&external_source=imdb_id'
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(find_TMDB) as response:
|
||||
data = await response.json()
|
||||
if 'success' not in data.keys():
|
||||
tmdb_status = "API Disponible ✅"
|
||||
tmdb_latency = str(round(measure_latency(
|
||||
host=TMDB_SITE, timeout=2.5)[0])) + "ms ⏱️"
|
||||
else:
|
||||
tmdb_status = "API no disponible ❌"
|
||||
tmdb_latency = "N/A ❌"
|
||||
|
||||
else:
|
||||
omdb_status = "API no disponible.❌"
|
||||
omdb_latency = "N/A ❌"
|
||||
tmdb_status = "API no disponible.❌"
|
||||
tmdb_latency = "N/A ❌"
|
||||
|
||||
await message.edit_text(self.STATUS_MESSAGE.format(db_status, db_latency, movie_number, omdb_status, omdb_latency, tmdb_status, tmdb_latency), parse_mode='markdown')
|
||||
|
||||
# /start command
|
||||
|
||||
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.message.reply_chat_action(action="typing")
|
||||
await update.message.reply_photo(photo="https://www.pngmart.com/files/15/Baby-Bender-PNG.png", caption=self.INTRO_MSG, parse_mode='markdown')
|
||||
|
||||
# /help command
|
||||
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.message.reply_text(self.HELP_MSG, parse_mode='markdown')
|
||||
|
||||
# Replying to text other than commands
|
||||
async def any_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
message_type: str = update.message.chat.type
|
||||
if message_type not in ['group', 'supergroup']:
|
||||
await update.message.reply_text(self.INVALID_FIND_MSG.format(update.message.text), parse_mode='markdown')
|
||||
|
||||
# Error Handling
|
||||
async def error(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
print(f'\nActualizar\n{update.message}\n\ncausa error {context.error}')
|
||||
await update.message.reply_text("*Lo siento, se encontró un error.*", parse_mode='markdown')
|
||||
|
||||
# /find command
|
||||
|
||||
async def find_title(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if len(self.memory) == 25:
|
||||
self.memory = []
|
||||
if len(self.movie_memory) == 25:
|
||||
self.movie_memory = []
|
||||
|
||||
if "".join(context.args) == "":
|
||||
await update.message.reply_text(self.FIND_MSG, parse_mode='markdown')
|
||||
|
||||
else:
|
||||
if "y=" in context.args[-1]:
|
||||
movie_name = " ".join(context.args[:-1])
|
||||
omdb_params = {
|
||||
"apikey": OMDB_API,
|
||||
"t": movie_name,
|
||||
"y": context.args[-1][2:]
|
||||
}
|
||||
else:
|
||||
movie_name = " ".join(context.args)
|
||||
omdb_params = {
|
||||
"apikey": OMDB_API,
|
||||
"t": movie_name,
|
||||
}
|
||||
|
||||
for item in self.memory:
|
||||
if movie_name == item["Title"].lower() or movie_name == item['Title']:
|
||||
movie_data = item
|
||||
break
|
||||
else:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(OMDB, params=omdb_params) as response:
|
||||
movie_data = await response.json()
|
||||
if movie_data["Response"] != "False":
|
||||
self.memory.append(movie_data)
|
||||
|
||||
if movie_data["Response"] != "False":
|
||||
data_str = f"🎬 *Título:* {movie_data['Title']} ({movie_data['Year']})\n\n" \
|
||||
f"📖 *Género:* {movie_data['Genre']}\n\n" \
|
||||
f"⭐ *Rating:* {movie_data['imdbRating']}/10\n\n" \
|
||||
f"🕤 *Duración:* {movie_data['Runtime']}\n\n" \
|
||||
f"🎭 *Actores:* {movie_data['Actors']}\n\n" \
|
||||
f"🧑 *Director:* {movie_data['Director']}\n\n" \
|
||||
f"🆔 *IMDB ID:* {movie_data['imdbID']}\n\n"
|
||||
|
||||
if movie_data['Poster'] != 'N/A':
|
||||
await update.message.reply_photo(photo=movie_data['Poster'])
|
||||
else:
|
||||
find_TMDB = f'https://api.themoviedb.org/3/find/{movie_data["imdbID"]}?api_key={TMDB_API}&external_source=imdb_id'
|
||||
|
||||
TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/original'
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(find_TMDB) as response:
|
||||
data = await response.json()
|
||||
if data['movie_results'] != []:
|
||||
Poster = data['movie_results'][0]['backdrop_path']
|
||||
URL = str(TMDB_IMAGE_BASE+Poster)
|
||||
await update.message.reply_photo(photo=URL)
|
||||
elif data['tv_results'] != []:
|
||||
Poster = data['tv_results'][0]['backdrop_path']
|
||||
URL = str(TMDB_IMAGE_BASE+Poster)
|
||||
await update.message.reply_photo(photo=URL)
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton("Plot", callback_data=f"{movie_data['Title']};plot"),
|
||||
InlineKeyboardButton("Ratings", callback_data=f"{movie_data['Title']};ratings")],
|
||||
[InlineKeyboardButton("Awards", callback_data=f"{movie_data['Title']};awards"),
|
||||
InlineKeyboardButton(
|
||||
"Languages", callback_data=f"{movie_data['Title']};languages"),
|
||||
InlineKeyboardButton("Rated", callback_data=f"{movie_data['Title']};rated")],
|
||||
[InlineKeyboardButton("IMDB page", url=f"{IMDB_LINK}{movie_data['imdbID']}"),
|
||||
InlineKeyboardButton("Trailer", url=await self.get_trailer_url(movie_data["imdbID"], movie_data['Title']))],
|
||||
[InlineKeyboardButton(
|
||||
"Get Movie", callback_data=f"{movie_data['Title']};getmovie")]
|
||||
]
|
||||
await update.message.reply_text(data_str, reply_markup=InlineKeyboardMarkup(buttons), parse_mode='markdown')
|
||||
|
||||
else:
|
||||
await update.message.reply_chat_action(action="typing")
|
||||
await update.message.reply_photo(photo='https://raw.githubusercontent.com/akkupy/movie_bot/main/assets/check_spelling.jpg', caption=self.MOVIE_NOT_FOUND, parse_mode='markdown')
|
||||
|
||||
@staticmethod
|
||||
async def get_trailer_url(imdb_id: str, Title: str) -> None:
|
||||
|
||||
find_TMDB = f'https://api.themoviedb.org/3/find/{imdb_id}?api_key={TMDB_API}&external_source=imdb_id'
|
||||
|
||||
YOUTUBE_BASE_URL = 'https://www.youtube.com/watch?v='
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(find_TMDB) as response:
|
||||
data = await response.json()
|
||||
if data['movie_results'] != []:
|
||||
TMDB_ID = data['movie_results'][0]['id']
|
||||
TYPE = 'movie'
|
||||
elif data['tv_results'] != []:
|
||||
TMDB_ID = data['tv_results'][0]['id']
|
||||
TYPE = 'tv'
|
||||
else:
|
||||
return f'https://www.youtube.com/results?search_query={Title}'
|
||||
|
||||
video_TMDB = f'https://api.themoviedb.org/3/{TYPE}/{TMDB_ID}/videos?api_key={TMDB_API}'
|
||||
async with session.get(video_TMDB) as response:
|
||||
data = await response.json()
|
||||
if data['results'] != []:
|
||||
video = data['results'][0]['key']
|
||||
else:
|
||||
return f'https://www.youtube.com/results?search_query={Title}'
|
||||
return YOUTUBE_BASE_URL+video
|
||||
|
||||
@staticmethod
|
||||
def get_rating(movie_json: dict) -> str:
|
||||
rating_str: str = ""
|
||||
for rating in movie_json["Ratings"]:
|
||||
rating_str += f"{rating['Source']}: {rating['Value']}\n"
|
||||
return f"*{movie_json['Title']} Ratings* ⭐\n\n{rating_str}"
|
||||
|
||||
@staticmethod
|
||||
def get_rated(movie_json: dict) -> str:
|
||||
return f"*{movie_json['Title']} Rated* 🔞\n\n{movie_json['Rated']}"
|
||||
|
||||
@staticmethod
|
||||
def get_plot(movie_json: dict) -> str:
|
||||
return f"*{movie_json['Title']} Plot* 📖\n\n{movie_json['Plot']}"
|
||||
|
||||
@staticmethod
|
||||
def get_languages(movie_json: dict) -> str:
|
||||
return f"*{movie_json['Title']} Languages* 🗣️\n\n{movie_json['Language']}"
|
||||
|
||||
@staticmethod
|
||||
def get_awards(movie_json: dict) -> str:
|
||||
return f"*{movie_json['Title']} Awards* 🏆\n\n{movie_json['Awards']}"
|
||||
|
||||
@staticmethod
|
||||
def get_movie(self, movie_json: dict) -> str:
|
||||
Flag = False
|
||||
for item in self.movie_memory:
|
||||
if movie_json['imdbID'] == item["imdb_id"]:
|
||||
file_data = item
|
||||
break
|
||||
|
||||
else:
|
||||
self.cursor.execute(
|
||||
"select count(*) from movie_data where imdb_id = '{}'".format(movie_json['imdbID']))
|
||||
if self.cursor.fetchone()[0] != 0:
|
||||
self.cursor.execute(
|
||||
"select * from movie_data where imdb_id = '{}'".format(movie_json['imdbID']))
|
||||
data = self.cursor.fetchone()
|
||||
file_data = {
|
||||
'imdb_id': data[0],
|
||||
'from_chat_id': data[1],
|
||||
'message_id': data[2],
|
||||
}
|
||||
self.movie_memory.append({
|
||||
'imdb_id': data[0],
|
||||
'from_chat_id': data[1],
|
||||
'message_id': data[2],
|
||||
})
|
||||
else:
|
||||
Flag = True
|
||||
from_chat_id = message_id = None
|
||||
return from_chat_id, message_id
|
||||
|
||||
if not Flag:
|
||||
from_chat_id, message_id = file_data['from_chat_id'], file_data['message_id']
|
||||
return from_chat_id, message_id
|
||||
|
||||
# Query Handler
|
||||
|
||||
async def query_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query.data
|
||||
await update.callback_query.answer()
|
||||
|
||||
title, kword = query.split(";")
|
||||
for item in self.memory:
|
||||
if title == item["Title"]:
|
||||
data = item
|
||||
break
|
||||
else:
|
||||
omdb_params = {
|
||||
"apikey": OMDB_API,
|
||||
"t": title,
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(OMDB, params=omdb_params) as result:
|
||||
data = await result.json()
|
||||
self.memory.append(data)
|
||||
if kword == "ratings":
|
||||
await update.callback_query.message.reply_text(self.get_rating(data), parse_mode='markdown')
|
||||
elif kword == "plot":
|
||||
await update.callback_query.message.reply_text(self.get_plot(data), parse_mode='markdown')
|
||||
elif kword == "rated":
|
||||
await update.callback_query.message.reply_text(self.get_rated(data), parse_mode='markdown')
|
||||
elif kword == "awards":
|
||||
await update.callback_query.message.reply_text(self.get_awards(data), parse_mode='markdown')
|
||||
elif kword == "languages":
|
||||
await update.callback_query.message.reply_text(self.get_languages(data), parse_mode='markdown')
|
||||
elif kword == "getmovie":
|
||||
from_chat_id, message_id = self.get_movie(self, data)
|
||||
if from_chat_id == None:
|
||||
await update.callback_query.message.reply_text(self.MOVIE_NOT_FOUND_MSG.format(data['Title'], data["imdbID"]), parse_mode='markdown')
|
||||
else:
|
||||
await update.callback_query.message._bot.forward_message(update.callback_query.message.chat.id, from_chat_id, message_id)
|
||||
|
||||
async def movie_saver(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
imdb_id = "".join(context.args)
|
||||
|
||||
if imdb_id == "":
|
||||
await update.message.reply_text(self.SAVE_MSG, parse_mode='markdown')
|
||||
|
||||
elif update.message.reply_to_message == None:
|
||||
await update.message.reply_text(self.SAVE_REPLY_MSG, parse_mode='markdown')
|
||||
else:
|
||||
message_id = update.message.reply_to_message.id
|
||||
|
||||
from_chat_id = update.message.chat.id
|
||||
|
||||
self.cursor.execute(
|
||||
"select count(*) from movie_data where imdb_id = '{}'".format(imdb_id))
|
||||
if self.cursor.fetchone()[0] == 0:
|
||||
self.cursor.execute("insert into movie_data values('{}',{},{})".format(
|
||||
imdb_id, from_chat_id, message_id))
|
||||
self.connection.commit()
|
||||
await update.message.reply_text('*Movie/Series saved on database.*\n', parse_mode='markdown')
|
||||
else:
|
||||
await update.message.reply_text('*Movie/Series already present on database.*\n', parse_mode='markdown')
|
||||
|
||||
async def movie_remover(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
imdb_id = "".join(context.args)
|
||||
|
||||
if imdb_id == "":
|
||||
await update.message.reply_text(self.REMOVE_MSG, parse_mode='markdown')
|
||||
|
||||
else:
|
||||
|
||||
self.cursor.execute(
|
||||
"select count(*) from movie_data where imdb_id = '{}'".format(imdb_id))
|
||||
if self.cursor.fetchone()[0] != 0:
|
||||
self.cursor.execute(
|
||||
"delete from movie_data where imdb_id = '{}'".format(imdb_id))
|
||||
self.connection.commit()
|
||||
await update.message.reply_text('*Movie/Series deleted from database.*\n', parse_mode='markdown')
|
||||
else:
|
||||
await update.message.reply_text('*Movie/Series not found on database.*\n', parse_mode='markdown')
|
||||
|
||||
count = 0
|
||||
for item in self.movie_memory:
|
||||
if imdb_id == item["imdb_id"]:
|
||||
del self.movie_memory[count]
|
||||
count += 1
|
||||
|
||||
async def movie_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
self.cursor.execute("select count(*) from movie_data")
|
||||
number = self.cursor.fetchone()[0]
|
||||
await update.message.reply_text(f'*{number} Movies/Series 🎬 found on database.*', parse_mode='markdown')
|
||||
25
catch-all/06_bots_telegram/07_movie2_bot/Dockerfile
Normal file
25
catch-all/06_bots_telegram/07_movie2_bot/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
# Utiliza una imagen base de Python en Alpine
|
||||
FROM python:3.12.4-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instala bash, herramientas de compilación y librerías necesarias
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
python -m venv /env && \
|
||||
/env/bin/pip install --upgrade pip
|
||||
|
||||
# Añade el archivo de requisitos a la imagen
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
|
||||
RUN /env/bin/pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
# Añade el resto del código de la aplicación
|
||||
COPY . /app
|
||||
|
||||
# Configura el entorno virtual
|
||||
ENV VIRTUAL_ENV=/env
|
||||
ENV PATH=/env/bin:$PATH
|
||||
|
||||
# Comando por defecto para ejecutar el bot
|
||||
CMD ["python", "main.py"]
|
||||
67
catch-all/06_bots_telegram/07_movie2_bot/README.md
Normal file
67
catch-all/06_bots_telegram/07_movie2_bot/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# QuizBot
|
||||
|
||||
__Basado en el repositorio de [CineMonster](https://github.com/RogueFairyStudios/CineMonster)__
|
||||
|
||||
Bot de Telegram con un juego basado en preguntas sobre películas. Resumen de las funcionalidades:
|
||||
|
||||
|
||||
## **Comandos Disponibles:**
|
||||
|
||||
1. **`/start`**:
|
||||
- **Descripción**: Inicia una nueva sesión de juego para el chat.
|
||||
- **Acciones**: Crea una nueva instancia de `Session` y la almacena en `SESSIONS`. Si se encuentra una clase `Quiz` en el módulo `quiz`, la inicializa para la sesión.
|
||||
|
||||
2. **`/roll`**:
|
||||
- **Descripción**: Lanza una pregunta de trivia sobre películas.
|
||||
- **Acciones**: Llama a `show` en el objeto `Quiz` de la sesión activa. Envía una imagen de una película al chat y establece el estado del juego en "running".
|
||||
|
||||
3. **`/leaderboard`**:
|
||||
- **Descripción**: Muestra la tabla de clasificación de los jugadores.
|
||||
- **Acciones**: Envía la tabla de clasificación actual a través del `messenger`. La tabla muestra los jugadores y sus puntos.
|
||||
|
||||
4. **`/repeat`**:
|
||||
- **Descripción**: Repite la última pregunta de trivia sobre películas.
|
||||
- **Acciones**: Envía de nuevo la imagen de la película al chat junto con la pregunta sobre el título de la película.
|
||||
|
||||
5. **`/cut`**:
|
||||
- **Descripción**: Permite que un jugador abandone el juego.
|
||||
- **Acciones**: Elimina al jugador de la sesión actual y notifica al chat que el jugador ha abandonado el juego.
|
||||
|
||||
6. **`/stop`**:
|
||||
- **Descripción**: Finaliza la sesión de juego actual.
|
||||
- **Acciones**: Elimina la sesión del chat actual de `SESSIONS` y notifica al chat que el juego ha terminado.
|
||||
|
||||
7. **`/check_resps`**:
|
||||
- **Descripción**: Verifica las respuestas enviadas por los jugadores.
|
||||
- **Acciones**: Compara la respuesta del usuario con la respuesta correcta de la película y actualiza el puntaje si la respuesta es correcta.
|
||||
|
||||
|
||||
## **Funcionalidades Adicionales:**
|
||||
|
||||
- **Manejo de Temporizadores**:
|
||||
- Utiliza `apscheduler` para ejecutar `update_all_timers` cada minuto, lo que actualiza los temporizadores de todas las sesiones y verifica la expiración del tiempo de juego.
|
||||
|
||||
- **Mensajería**:
|
||||
- Usa un objeto `messenger` para enviar mensajes y fotos a los usuarios en el chat, manejando la comunicación con Telegram.
|
||||
|
||||
- **Gestión de Jugadores**:
|
||||
- Permite agregar y quitar jugadores de la sesión. Actualiza el puntaje de los jugadores en función de sus respuestas correctas.
|
||||
|
||||
- **Control de Estado del Juego**:
|
||||
- Los estados del juego (`running`, `stopped`, `timed_out`) controlan el flujo del juego, incluyendo la verificación de respuestas y el manejo de tiempos de espera.
|
||||
|
||||
- **Manejo de Errores**:
|
||||
- Maneja errores durante el proceso de actualización y respuesta, notificando a los usuarios en caso de problemas con la pregunta de trivia o el estado de la sesión.
|
||||
|
||||
|
||||
## **Estructura del Código:**
|
||||
|
||||
1. **`Session`**:
|
||||
- Maneja la lógica del juego, incluidos los jugadores, el estado de la sesión, y los temporizadores.
|
||||
|
||||
2. **`Quiz`**:
|
||||
- Se encarga de la lógica relacionada con las preguntas sobre películas, incluida la selección de una película al azar y la verificación de las respuestas.
|
||||
|
||||
3. **`Server`**:
|
||||
- Configura el bot de Telegram, maneja los comandos y los eventos, y gestiona las sesiones de juego.
|
||||
|
||||
1
catch-all/06_bots_telegram/07_movie2_bot/_config.yml
Normal file
1
catch-all/06_bots_telegram/07_movie2_bot/_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-slate
|
||||
@@ -0,0 +1,40 @@
|
||||
from miners import Miner
|
||||
from random import *
|
||||
|
||||
|
||||
class Collection:
|
||||
|
||||
movie_list = ''
|
||||
|
||||
def __init__(self, miner, type):
|
||||
self.miner = miner
|
||||
self.type = type
|
||||
|
||||
def top_250(self):
|
||||
self.movie_list = self.miner.get_top(250)
|
||||
|
||||
def general(self):
|
||||
pass
|
||||
|
||||
def get_rand_movie(self):
|
||||
movie = None
|
||||
|
||||
while movie is None:
|
||||
if self.type is None:
|
||||
number = str(randrange(1, 99999))
|
||||
if len(number) < 7:
|
||||
number = '0' * (7 - len(number)) + number
|
||||
movie_id = 'tt' + number
|
||||
else:
|
||||
self.top_250()
|
||||
number = randrange(0, len(self.movie_list) - 1)
|
||||
movie_id = self.movie_list[number]['tconst']
|
||||
|
||||
images, movie = self.miner.get_movie_by_id(movie_id)
|
||||
print(movie['base']['title'])
|
||||
if images is not None:
|
||||
if images['totalImageCount'] < 1:
|
||||
movie = None
|
||||
|
||||
return movie, images
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from collection.Collection import Collection
|
||||
@@ -0,0 +1 @@
|
||||
from conf.config import Config, ProductionConfig, DevelopmentConfig, TestingConfig
|
||||
31
catch-all/06_bots_telegram/07_movie2_bot/conf/config.py
Normal file
31
catch-all/06_bots_telegram/07_movie2_bot/conf/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv('.env')
|
||||
|
||||
|
||||
class Config(object):
|
||||
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
DATABASE_URI = 'sqlite://:memory:'
|
||||
DATABASE_URL = os.getenv('DATABASE_URL')
|
||||
TELEGRAM_BOT_API = os.getenv('TELEGRAM_BOT_API')
|
||||
LOG_FILE = 'logs/movie2_bot.log'
|
||||
QUIZ_LANG = 'es'
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
DATABASE_URI = 'mysql://user@localhost/foo'
|
||||
SESSION_EXPIRATION_TIME = 30
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
SESSION_EXPIRATION_TIME = 10
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
TESTING = True
|
||||
22
catch-all/06_bots_telegram/07_movie2_bot/docker-compose.yaml
Normal file
22
catch-all/06_bots_telegram/07_movie2_bot/docker-compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# version: '3.7'
|
||||
|
||||
services:
|
||||
movie2_bot:
|
||||
env_file:
|
||||
- .env
|
||||
image: movie2_bot_python:latest
|
||||
container_name: movie2_bot
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Madrid
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ./movie2_bot_data:/app/db
|
||||
- ./logs:/app/logs
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
movie2_bot_data:
|
||||
logs:
|
||||
@@ -0,0 +1 @@
|
||||
from interfaces.telegram.messenger import Messenger as TelegramMessenger
|
||||
@@ -0,0 +1,45 @@
|
||||
from telegram.constants import ParseMode
|
||||
import telegram.ext
|
||||
|
||||
class Messenger:
|
||||
formats = {
|
||||
'regular': "*%s*",
|
||||
'caption': "*%s*",
|
||||
'title': "=+= *%s* =+=",
|
||||
'highlight': "--+ %s +--",
|
||||
'bold': "*%s* %s"
|
||||
}
|
||||
|
||||
def __init__(self, bot, logger):
|
||||
self.logger = logger
|
||||
self.logger.debug("Started...")
|
||||
self.bot = bot
|
||||
|
||||
def send_msg(self, chat_id, msg, type_msg='regular'):
|
||||
if type_msg not in self.formats:
|
||||
self.logger.error(f"Invalid message type: {type_msg}")
|
||||
return
|
||||
|
||||
formatted_msg = self.format(type_msg, msg)
|
||||
try:
|
||||
self.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=formatted_msg,
|
||||
parse_mode=ParseMode.MARKDOWN_V2
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error sending message: {e}")
|
||||
|
||||
def format(self, type_msg, msg):
|
||||
return self.formats[type_msg] % msg
|
||||
|
||||
def send_photo(self, chat_id, photo, caption):
|
||||
try:
|
||||
self.bot.send_photo(
|
||||
chat_id=chat_id,
|
||||
photo=photo,
|
||||
caption=caption,
|
||||
parse_mode=ParseMode.MARKDOWN_V2
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error sending photo: {e}")
|
||||
11
catch-all/06_bots_telegram/07_movie2_bot/main.py
Normal file
11
catch-all/06_bots_telegram/07_movie2_bot/main.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from server import Server
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Server()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
main()
|
||||
20
catch-all/06_bots_telegram/07_movie2_bot/miners/Miner.py
Normal file
20
catch-all/06_bots_telegram/07_movie2_bot/miners/Miner.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class Miner(object):
|
||||
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
handle = None
|
||||
|
||||
@abstractmethod
|
||||
def top_list(self, number):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_movie_id(self, index):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_movie_by_id(self, movie_id):
|
||||
pass
|
||||
@@ -0,0 +1,2 @@
|
||||
from miners.Miner import Miner
|
||||
from miners.imdb.ImdbMiner import IMDB
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user