You've already forked Curso-lenguaje-python
Compare commits
152 Commits
daabecd4a1
...
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 | |||
|
|
af9e0b66d9 | ||
|
|
219078c375 | ||
| db00df2e56 | |||
| a320317a9e | |||
| 61d38a9b7b | |||
| a0d26af9ed | |||
| 6fb5471334 | |||
| 08d20fee61 | |||
| d8426a91d9 | |||
| 398494e2f5 | |||
| d6a26ea924 | |||
| 40d20c3bcf | |||
| 8afad81939 | |||
| 2ff37fdb2b | |||
| a8c1199c90 | |||
| 94c89522d1 | |||
| 7b2cd6c376 | |||
| 6677000878 | |||
| 2cd9b703b2 | |||
| d34da87ee0 | |||
| 8bb0639340 |
33
.gitignore
vendored
33
.gitignore
vendored
@@ -127,9 +127,11 @@ celerybeat.pid
|
|||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
*.env
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
myenv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.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
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.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/*
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
01_gestor_paquetes.py
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from collections import Counter
|
||||||
|
import statistics
|
||||||
|
import pandas as pd
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# 1. Lee esta URL y encuentra las 10 palabras más frecuentes.
|
||||||
|
# Romeo y Julieta: 'http://www.gutenberg.org/files/1112/1112.txt'
|
||||||
|
|
||||||
|
url_romeo_julieta = 'http://www.gutenberg.org/files/1112/1112.txt'
|
||||||
|
response_romeo_julieta = requests.get(url_romeo_julieta)
|
||||||
|
words = response_romeo_julieta.text.split()
|
||||||
|
word_counts = Counter(words)
|
||||||
|
top_10_words = word_counts.most_common(10)
|
||||||
|
|
||||||
|
print("10 palabras más frecuentes:")
|
||||||
|
for word, count in top_10_words:
|
||||||
|
print(word, "-", count)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 2. Lee la API de gatos y la URL:
|
||||||
|
# 'https://api.thecatapi.com/v1/breeds'
|
||||||
|
# y encuentra:
|
||||||
|
|
||||||
|
url_gatos = 'https://api.thecatapi.com/v1/breeds'
|
||||||
|
response_gatos = requests.get(url_gatos)
|
||||||
|
data_gatos = response_gatos.json()
|
||||||
|
|
||||||
|
# - El mínimo, máximo, promedio,
|
||||||
|
# mediana y desviación estándar
|
||||||
|
# del peso de los gatos en unidades métricas.
|
||||||
|
|
||||||
|
weights = [cat['weight']['metric'] for cat in data_gatos]
|
||||||
|
weights = [float(w.split()[0]) for w in weights]
|
||||||
|
min_weight = min(weights)
|
||||||
|
max_weight = max(weights)
|
||||||
|
avg_weight = statistics.mean(weights)
|
||||||
|
med_weight = statistics.median(weights)
|
||||||
|
std_weight = statistics.stdev(weights)
|
||||||
|
|
||||||
|
print("Peso de los gatos en unidades métricas:")
|
||||||
|
print("Mínimo:", min_weight)
|
||||||
|
print("Máximo:", max_weight)
|
||||||
|
print("Promedio:", avg_weight)
|
||||||
|
print("Mediana:", med_weight)
|
||||||
|
print("Desviación estándar:", std_weight)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# - El mínimo, máximo, promedio,
|
||||||
|
# mediana y desviación estándar
|
||||||
|
# de la esperanza de vida
|
||||||
|
# de los gatos en años.
|
||||||
|
|
||||||
|
lifespans = [cat['life_span'] for cat in data_gatos]
|
||||||
|
lifespans = [int(l.split()[0]) for l in lifespans if l != '']
|
||||||
|
min_lifespan = min(lifespans)
|
||||||
|
max_lifespan = max(lifespans)
|
||||||
|
avg_lifespan = statistics.mean(lifespans)
|
||||||
|
med_lifespan = statistics.median(lifespans)
|
||||||
|
std_lifespan = statistics.stdev(lifespans)
|
||||||
|
|
||||||
|
print("Esperanza de vida de los gatos en años:")
|
||||||
|
print("Mínimo:", min_lifespan)
|
||||||
|
print("Máximo:", max_lifespan)
|
||||||
|
print("Promedio:", avg_lifespan)
|
||||||
|
print("Mediana:", med_lifespan)
|
||||||
|
print("Desviación estándar:", std_lifespan)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# - Crea una tabla de frecuencias
|
||||||
|
# de países y razas de gatos.
|
||||||
|
|
||||||
|
country_counts = Counter([cat['origin']
|
||||||
|
for cat in data_gatos if cat['origin'] != ''])
|
||||||
|
breed_counts = Counter([cat['name'] for cat in data_gatos])
|
||||||
|
|
||||||
|
print("Tabla de frecuencias de países:")
|
||||||
|
print(pd.DataFrame.from_dict(country_counts, orient='index'))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Tabla de frecuencias de razas:")
|
||||||
|
print(pd.DataFrame.from_dict(breed_counts, orient='index'))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 3. Lee la [API de países](https://restcountries.eu/rest/v2/all)
|
||||||
|
# y encuentra:
|
||||||
|
|
||||||
|
# El enlace está caído, uso otra versión
|
||||||
|
url_countries = 'https://restcountries.com/v3.1/all'
|
||||||
|
response_countries = requests.get(url_countries)
|
||||||
|
data_countries = response_countries.json()
|
||||||
|
|
||||||
|
# - Los 10 países más grandes.
|
||||||
|
|
||||||
|
df = pd.DataFrame(data_countries)
|
||||||
|
df['area'] = pd.to_numeric(df['area'])
|
||||||
|
largest_countries = df.nlargest(10, 'area')
|
||||||
|
|
||||||
|
print("Los 10 países más grandes:")
|
||||||
|
for country in largest_countries['name']:
|
||||||
|
print(country['common'])
|
||||||
|
print()
|
||||||
|
|
||||||
|
# - Los 10 idiomas más hablados.
|
||||||
|
|
||||||
|
most_spoken_languages = df.explode('languages').groupby(
|
||||||
|
'languages').size().nlargest(10)
|
||||||
|
|
||||||
|
print("Los 10 idiomas más hablados:")
|
||||||
|
print(most_spoken_languages)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# - El número total de idiomas en la API de países.
|
||||||
|
|
||||||
|
total_languages = len(df.explode('languages')['languages'].unique())
|
||||||
|
|
||||||
|
print("Número total de idiomas en la API de países:", total_languages)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# 4. La Universidad de California en Irvine (UCI)
|
||||||
|
# es uno de los lugares más comunes para obtener
|
||||||
|
# conjuntos de datos para ciencia de datos
|
||||||
|
# y aprendizaje automático.
|
||||||
|
# Lee el contenido de UCI
|
||||||
|
# (https://archive.ics.uci.edu/ml/datasets.php).
|
||||||
|
# Sin bibliotecas adicionales, puede ser difícil,
|
||||||
|
# por lo que puedes intentarlo con BeautifulSoup4.
|
||||||
|
|
||||||
|
url = 'https://archive.ics.uci.edu/ml/datasets.php'
|
||||||
|
response = requests.get(url)
|
||||||
|
soup = BeautifulSoup(response.content, 'html.parser')
|
||||||
|
links = [a['href'] for a in soup.find_all('a') if a.has_attr('href')]
|
||||||
@@ -20,4 +20,6 @@ Documento original en inglés: [python package manager](https://github.com/Asabe
|
|||||||
|
|
||||||
4. La Universidad de California en Irvine (UCI) es uno de los lugares más comunes para obtener conjuntos de datos para ciencia de datos y aprendizaje automático. Lee el contenido de UCI (https://archive.ics.uci.edu/ml/datasets.php). Sin bibliotecas adicionales, puede ser difícil, por lo que puedes intentarlo con BeautifulSoup4.
|
4. La Universidad de California en Irvine (UCI) es uno de los lugares más comunes para obtener conjuntos de datos para ciencia de datos y aprendizaje automático. Lee el contenido de UCI (https://archive.ics.uci.edu/ml/datasets.php). Sin bibliotecas adicionales, puede ser difícil, por lo que puedes intentarlo con BeautifulSoup4.
|
||||||
|
|
||||||
|
[Solución](01_gestor_paquetes.py)
|
||||||
|
|
||||||
[<< Day 19](../19_Manipulación_de_archivos/README.md) | [Day 21 >>](../21_Clases_y_objetos/README.md)
|
[<< Day 19](../19_Manipulación_de_archivos/README.md) | [Day 21 >>](../21_Clases_y_objetos/README.md)
|
||||||
|
|||||||
137
30-days-of-python/21_Clases_y_objetos/01_clases_objetos.py
Normal file
137
30-days-of-python/21_Clases_y_objetos/01_clases_objetos.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
01_clases_objetos.py
|
||||||
|
"""
|
||||||
|
import statistics
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Python tiene el módulo llamado estadísticas
|
||||||
|
# y podemos usar este módulo para realizar todos
|
||||||
|
# los cálculos estadísticos. Sin embargo,
|
||||||
|
# para aprender a crear funciones y reutilizar funciones,
|
||||||
|
# intentemos desarrollar un programa
|
||||||
|
# que calcule la medida de tendencia central de una muestra
|
||||||
|
# (media, mediana, moda) y la medida de variabilidad
|
||||||
|
# (rango, varianza, desviación estándar).
|
||||||
|
# Además de esas medidas, encuentra el mínimo, máximo,
|
||||||
|
# recuento, percentil y distribución de frecuencia de la muestra.
|
||||||
|
# Puedes crear una clase llamada Estadísticas
|
||||||
|
# y crear todas las funciones que realicen cálculos estadísticos
|
||||||
|
# como métodos para la clase Estadísticas.
|
||||||
|
|
||||||
|
class Estadisticas:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
def sum(self):
|
||||||
|
return sum(self.data)
|
||||||
|
|
||||||
|
def min(self):
|
||||||
|
return min(self.data)
|
||||||
|
|
||||||
|
def max(self):
|
||||||
|
return max(self.data)
|
||||||
|
|
||||||
|
def range(self):
|
||||||
|
return max(self.data) - min(self.data)
|
||||||
|
|
||||||
|
def mean(self):
|
||||||
|
return statistics.mean(self.data)
|
||||||
|
|
||||||
|
def median(self):
|
||||||
|
return statistics.median(self.data)
|
||||||
|
|
||||||
|
def mode(self):
|
||||||
|
mode = statistics.mode(self.data)
|
||||||
|
count = self.data.count(mode)
|
||||||
|
return {'moda': mode, 'recuento': count}
|
||||||
|
|
||||||
|
def std(self):
|
||||||
|
return statistics.stdev(self.data)
|
||||||
|
|
||||||
|
def var(self):
|
||||||
|
return statistics.variance(self.data)
|
||||||
|
|
||||||
|
def freq_dist(self):
|
||||||
|
freq = {}
|
||||||
|
for value in self.data:
|
||||||
|
freq[value] = freq.get(value, 0) + 1
|
||||||
|
freq_dist = [(freq[value], value) for value in freq]
|
||||||
|
freq_dist.sort(reverse=True)
|
||||||
|
return freq_dist
|
||||||
|
|
||||||
|
|
||||||
|
edades = [
|
||||||
|
31, 26, 34, 37, 27,
|
||||||
|
26, 32, 32, 26, 27,
|
||||||
|
27, 24, 32, 33, 27,
|
||||||
|
25, 26, 38, 37, 31,
|
||||||
|
34, 24, 33, 29, 26
|
||||||
|
]
|
||||||
|
|
||||||
|
estadisticas = Estadisticas(edades)
|
||||||
|
|
||||||
|
print('Recuento:', estadisticas.count())
|
||||||
|
print('Suma: ', estadisticas.sum())
|
||||||
|
print('Mínimo: ', estadisticas.min())
|
||||||
|
print('Máximo: ', estadisticas.max())
|
||||||
|
print('Rango: ', estadisticas.range())
|
||||||
|
print('Media: ', estadisticas.mean())
|
||||||
|
print('Mediana: ', estadisticas.median())
|
||||||
|
print('Moda:')
|
||||||
|
for key, value in estadisticas.mode().items():
|
||||||
|
print(f'\t - {key}: {value}')
|
||||||
|
print('Desviación Estándar: ', estadisticas.std())
|
||||||
|
print('Varianza: ', estadisticas.var())
|
||||||
|
freq_dist = estadisticas.freq_dist()
|
||||||
|
print('Distribución de Frecuencia:')
|
||||||
|
for freq, value in freq_dist:
|
||||||
|
print(f'\t - {freq}, {value}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 2. Crea una clase llamada CuentaPersona.
|
||||||
|
# Tiene propiedades de nombre, apellido,
|
||||||
|
# ingresos, gastos y tiene métodos de total_ingreso,
|
||||||
|
# total_gasto, info_cuenta, agregar_ingreso,
|
||||||
|
# agregar_gasto y saldo_cuenta.
|
||||||
|
# Ingresos es un conjunto de ingresos y su descripción.
|
||||||
|
# Lo mismo ocurre con los gastos.
|
||||||
|
|
||||||
|
|
||||||
|
class CuentaPersona:
|
||||||
|
def __init__(self, nombre, apellido):
|
||||||
|
self.nombre = nombre
|
||||||
|
self.apellido = apellido
|
||||||
|
self.ingresos = {}
|
||||||
|
self.gastos = {}
|
||||||
|
|
||||||
|
def total_ingreso(self):
|
||||||
|
return sum(self.ingresos.values())
|
||||||
|
|
||||||
|
def total_gasto(self):
|
||||||
|
return sum(self.gastos.values())
|
||||||
|
|
||||||
|
def info_cuenta(self):
|
||||||
|
print(f'Nombre: {self.nombre} {self.apellido}')
|
||||||
|
print(f'Total de ingresos: {self.total_ingreso()}')
|
||||||
|
print(f'Total de gastos: {self.total_gasto()}')
|
||||||
|
print(f'Saldo de cuenta: {self.total_ingreso() - self.total_gasto()}')
|
||||||
|
|
||||||
|
def agregar_ingreso(self, descripcion, monto):
|
||||||
|
self.ingresos[descripcion] = monto
|
||||||
|
|
||||||
|
def agregar_gasto(self, descripcion, monto):
|
||||||
|
self.gastos[descripcion] = monto
|
||||||
|
|
||||||
|
def saldo_cuenta(self):
|
||||||
|
return self.total_ingreso() - self.total_gasto()
|
||||||
|
|
||||||
|
|
||||||
|
cuenta = CuentaPersona('Juan', 'Pérez')
|
||||||
|
cuenta.agregar_ingreso('Salario', 5000)
|
||||||
|
cuenta.agregar_ingreso('Venta de acciones', 2000)
|
||||||
|
cuenta.agregar_gasto('Alquiler', 1500)
|
||||||
|
cuenta.agregar_gasto('Comida', 500)
|
||||||
|
cuenta.info_cuenta()
|
||||||
@@ -44,4 +44,6 @@ Distribución de Frecuencia: [(20.0, 26), (16.0, 27), (12.0, 32), (8.0, 37), (8.
|
|||||||
|
|
||||||
1. Crea una clase llamada CuentaPersona. Tiene propiedades de nombre, apellido, ingresos, gastos y tiene métodos de total_ingreso, total_gasto, info_cuenta, agregar_ingreso, agregar_gasto y saldo_cuenta. Ingresos es un conjunto de ingresos y su descripción. Lo mismo ocurre con los gastos.
|
1. Crea una clase llamada CuentaPersona. Tiene propiedades de nombre, apellido, ingresos, gastos y tiene métodos de total_ingreso, total_gasto, info_cuenta, agregar_ingreso, agregar_gasto y saldo_cuenta. Ingresos es un conjunto de ingresos y su descripción. Lo mismo ocurre con los gastos.
|
||||||
|
|
||||||
|
[Solución](01_clases_objetos.py)
|
||||||
|
|
||||||
[<< Day 20](../20_Gestor_de_paquetes_de_Python/README.md) | [Day 22 >>](../22_Web_scraping/README.md)
|
[<< Day 20](../20_Gestor_de_paquetes_de_Python/README.md) | [Day 22 >>](../22_Web_scraping/README.md)
|
||||||
|
|||||||
32
30-days-of-python/22_Web_scraping/01_web_scraping.py
Normal file
32
30-days-of-python/22_Web_scraping/01_web_scraping.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Realiza un raspado web del siguiente sitio web
|
||||||
|
# y guarda los datos en un archivo JSON
|
||||||
|
# (URL = 'http://www.bu.edu/president/boston-university-facts-stats/').
|
||||||
|
|
||||||
|
|
||||||
|
url = 'http://www.bu.edu/president/boston-university-facts-stats/'
|
||||||
|
response = requests.get(url)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
for item in soup.find_all('div', {'class': 'facts-wrapper'}):
|
||||||
|
|
||||||
|
section_name = item.find('h5').get_text().strip()
|
||||||
|
section_data = {}
|
||||||
|
|
||||||
|
for li in item.find_all('li'):
|
||||||
|
key = li.find('p', {'class': 'text'}).get_text().strip()
|
||||||
|
value = li.find('span', {'class': 'value'}).get_text().strip()
|
||||||
|
section_data[key] = value
|
||||||
|
|
||||||
|
data[section_name] = section_data
|
||||||
|
|
||||||
|
with open('bu_stats.json', 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
print("Datos guardados en bu_stats.json")
|
||||||
34
30-days-of-python/22_Web_scraping/02_web_scraping.py
Normal file
34
30-days-of-python/22_Web_scraping/02_web_scraping.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
01_web_scraping.py
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 2. Extrae la tabla de esta URL
|
||||||
|
# (https://archive.ics.uci.edu/ml/datasets.php)
|
||||||
|
# y conviértela en un archivo JSON.
|
||||||
|
|
||||||
|
url = 'https://archive.ics.uci.edu/'
|
||||||
|
response = requests.get(url)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
for section in soup.find_all('section', class_='rounded-box'):
|
||||||
|
|
||||||
|
section_name = section.h1.get_text().strip()
|
||||||
|
section_data = {}
|
||||||
|
|
||||||
|
for div in section.find_all('div', class_='rounded-box'):
|
||||||
|
key = div.find('a', {'class': 'link-hover'}).get_text().strip()
|
||||||
|
value = div.find('p', {'class': 'truncate'}).get_text().strip()
|
||||||
|
section_data[key] = value
|
||||||
|
|
||||||
|
data[section_name] = section_data
|
||||||
|
|
||||||
|
|
||||||
|
with open('uci_datasets.json', 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
print("Datos guardados en uci_datasets.json")
|
||||||
41
30-days-of-python/22_Web_scraping/03_web_scraping.py
Normal file
41
30-days-of-python/22_Web_scraping/03_web_scraping.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import json
|
||||||
|
|
||||||
|
url = 'https://en.wikipedia.org/wiki/List_of_presidents_of_the_United_States'
|
||||||
|
response = requests.get(url)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Encuentra la tabla por su clase
|
||||||
|
table = soup.find('table', {'class': 'wikitable'})
|
||||||
|
|
||||||
|
# Encuentra todas las filas en la tabla
|
||||||
|
rows = table.find_all('tr')
|
||||||
|
|
||||||
|
# Extrae los encabezados de la primera fila
|
||||||
|
headers = [header.get_text().strip()
|
||||||
|
for header in rows[0].find_all(['th', 'td'])]
|
||||||
|
|
||||||
|
# Inicializa una lista para almacenar los datos
|
||||||
|
data = []
|
||||||
|
|
||||||
|
# Itera a través de las filas a partir de la segunda (índice 1)
|
||||||
|
for row in rows[1:]:
|
||||||
|
# Encuentra todas las celdas en la fila
|
||||||
|
cells = row.find_all(['th', 'td'])
|
||||||
|
|
||||||
|
# Verifica que haya celdas suficientes en la fila
|
||||||
|
if cells and len(cells) >= len(headers):
|
||||||
|
president = {}
|
||||||
|
for i, header in enumerate(headers):
|
||||||
|
# Asigna el valor de la celda al encabezado correspondiente
|
||||||
|
cell_data = cells[i].find(string=True) if cells[i].find(
|
||||||
|
string=True) else cells[i].find('a')['title'].strip()
|
||||||
|
president[header.lower()] = cell_data
|
||||||
|
data.append(president)
|
||||||
|
|
||||||
|
# Guarda los datos como JSON
|
||||||
|
with open('us_presidents.json', 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
print("Datos guardados en us_presidents.json")
|
||||||
@@ -6,8 +6,14 @@ Documento original en inglés: [Web Scraping](https://github.com/Asabeneh/30-Day
|
|||||||
|
|
||||||
1. Realiza un raspado web del siguiente sitio web y guarda los datos en un archivo JSON (URL = 'http://www.bu.edu/president/boston-university-facts-stats/').
|
1. Realiza un raspado web del siguiente sitio web y guarda los datos en un archivo JSON (URL = 'http://www.bu.edu/president/boston-university-facts-stats/').
|
||||||
|
|
||||||
|
[Solución](01_web_scraping.py)
|
||||||
|
|
||||||
2. Extrae la tabla de esta URL (https://archive.ics.uci.edu/ml/datasets.php) y conviértela en un archivo JSON.
|
2. Extrae la tabla de esta URL (https://archive.ics.uci.edu/ml/datasets.php) y conviértela en un archivo JSON.
|
||||||
|
|
||||||
|
[Solución](02_web_scraping.py)
|
||||||
|
|
||||||
3. Realiza un raspado web de la tabla de presidentes y guarda los datos como JSON (https://en.wikipedia.org/wiki/List_of_presidents_of_the_United_States). La tabla no está muy estructurada y el proceso de raspado puede llevar mucho tiempo.
|
3. Realiza un raspado web de la tabla de presidentes y guarda los datos como JSON (https://en.wikipedia.org/wiki/List_of_presidents_of_the_United_States). La tabla no está muy estructurada y el proceso de raspado puede llevar mucho tiempo.
|
||||||
|
|
||||||
|
[Solución](03_web_scraping.py)
|
||||||
|
|
||||||
[<< Day 21](../21_Clases_y_objetos/README.md) | [Day 23 >>](../23_Entorno_virtual/README.md)
|
[<< Day 21](../21_Clases_y_objetos/README.md) | [Day 23 >>](../23_Entorno_virtual/README.md)
|
||||||
|
|||||||
65
30-days-of-python/22_Web_scraping/bu_stats.json
Normal file
65
30-days-of-python/22_Web_scraping/bu_stats.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"Community": {
|
||||||
|
"Student Body": "37,557",
|
||||||
|
"Living Alumni": "431,000+",
|
||||||
|
"Total Employees": "10,674",
|
||||||
|
"Faculty": "4,309",
|
||||||
|
"Nondegree Students": "1,337",
|
||||||
|
"Graduate & Professional Students": "18,476",
|
||||||
|
"Undergraduate Students": "17,744"
|
||||||
|
},
|
||||||
|
"Campus": {
|
||||||
|
"Classrooms": "848",
|
||||||
|
"Buildings": "343",
|
||||||
|
"Laboratories": "1,481",
|
||||||
|
"Libraries": "13",
|
||||||
|
"Campus Area (acres)": "140"
|
||||||
|
},
|
||||||
|
"Academics": {
|
||||||
|
"Study Abroad Programs": "80+",
|
||||||
|
"Average Class Size": "30",
|
||||||
|
"Faculty": "4,309",
|
||||||
|
"Student/Faculty Ratio": "11:1",
|
||||||
|
"Schools and Colleges": "17",
|
||||||
|
"Programs of Study": "300+"
|
||||||
|
},
|
||||||
|
"Grant & Contract Awards": {
|
||||||
|
"Research Expenditures (FY22)": "$630.7M",
|
||||||
|
"Research Awards": "$674M",
|
||||||
|
"BMC Clinical Research Grants (FY22)": "$82M"
|
||||||
|
},
|
||||||
|
"Undergraduate Financial Aid & Scholarships": {
|
||||||
|
"Average Total Need-Based Financial Aid": "$57,237",
|
||||||
|
"Average Need-Based Grant/Scholarship": "$53,029",
|
||||||
|
"Grants & Scholarships (need-based)": "$388.4M",
|
||||||
|
"Grants & Scholarships (non-need-based)": "$26.5M"
|
||||||
|
},
|
||||||
|
"Student Life": {
|
||||||
|
"Community Service Hours": "130,000+",
|
||||||
|
"Alternative Service Breaks Participants": "65+",
|
||||||
|
"BU on Social": "new accounts daily",
|
||||||
|
"Cultural & Religious Organizations": "80+",
|
||||||
|
"Community Service & Justice Organizations": "70+",
|
||||||
|
"Academic & Professional Organizations": "140+",
|
||||||
|
"Art & Performance Organizations": "60+",
|
||||||
|
"Student Organizations": "450+",
|
||||||
|
"First-Year Student Outreach Project Volunteers": "400+"
|
||||||
|
},
|
||||||
|
"Research": {
|
||||||
|
"Faculty Publications": "7,000+",
|
||||||
|
"Student UROP Participants": "450+",
|
||||||
|
"Centers & Institutes": "130+"
|
||||||
|
},
|
||||||
|
"International Community": {
|
||||||
|
"Global Initiatives": "300+",
|
||||||
|
"Cultural Student Groups": "60+",
|
||||||
|
"Alumni Countries": "180+",
|
||||||
|
"International Students": "10,000+"
|
||||||
|
},
|
||||||
|
"Athletics": {
|
||||||
|
"Intramural Sports & Tournaments": "12+",
|
||||||
|
"Club and Intramural Sports Participants": "7,000+",
|
||||||
|
"Club Sports": "36",
|
||||||
|
"Varsity Sports": "24"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
30-days-of-python/22_Web_scraping/uci_datasets.json
Normal file
18
30-days-of-python/22_Web_scraping/uci_datasets.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"Popular Datasets": {
|
||||||
|
"Iris": "A small classic dataset from Fisher, 1936. One of the earliest known datasets used for evaluating classification methods.",
|
||||||
|
"Heart Disease": "4 databases: Cleveland, Hungary, Switzerland, and the VA Long Beach",
|
||||||
|
"Adult": "Predict whether income exceeds $50K/yr based on census data. Also known as \"Census Income\" dataset.",
|
||||||
|
"Wine": "Using chemical analysis to determine the origin of wines",
|
||||||
|
"Dry Bean Dataset": "Images of 13,611 grains of 7 different registered dry beans were taken with a high-resolution camera. A total of 16 features; 12 dimensions and 4 shape forms, were obtained from the grains.",
|
||||||
|
"Diabetes": "This diabetes dataset is from AIM '94"
|
||||||
|
},
|
||||||
|
"New Datasets": {
|
||||||
|
"TCGA Kidney Cancers": "The TCGA Kidney Cancers Dataset is a bulk RNA-seq dataset that contains transcriptome profiles of patients diagnosed with three different subtypes of kidney cancers. This dataset can be used to make predictions about the specific subtype of kidney cancers given the normalized transcriptome profile data, as well as providing a hands-on experience on large and sparse genomic information.",
|
||||||
|
"CDC Diabetes Health Indicators": "The Diabetes Health Indicators Dataset contains healthcare statistics and lifestyle survey information about people in general along with their diagnosis of diabetes. The 35 features consist of some demographics, lab test results, and answers to survey questions for each patient. The target variable for classification is whether a patient has diabetes, is pre-diabetic, or healthy.",
|
||||||
|
"AIDS Clinical Trials Group Study 175": "The AIDS Clinical Trials Group Study 175 Dataset contains healthcare statistics and categorical information about patients who have been diagnosed with AIDS. This dataset was initially published in 1996. The prediction task is to predict whether or not each patient died within a certain window of time or not.",
|
||||||
|
"National Health and Nutrition Health Survey 2013-2014 (NHANES) Age Prediction Subset": "The National Health and Nutrition Examination Survey (NHANES), administered by the Centers for Disease Control and Prevention (CDC), collects extensive health and nutritional information from a diverse U.S. population. Though expansive, the dataset is often too broad for specific analytical purposes. In this sub-dataset, we narrow our focus to predicting respondents' age by extracting a subset of features from the larger NHANES dataset. These selected features include physiological measurements, lifestyle choices, and biochemical markers, which were hypothesized to have strong correlations with age.",
|
||||||
|
"Large-scale Wave Energy Farm": "Wave energy is a rapidly advancing and promising renewable energy source that holds great potential for addressing the challenges of global warming and climate change. However, optimizing energy output in large wave farms presents a complex problem due to the expensive calculations required to account for hydrodynamic interactions between wave energy converters (WECs). Developing a fast and accurate surrogate model is crucial to overcome these challenges. In light of this, we have compiled an extensive WEC dataset that includes 54,000 and 9,600 configurations involving 49 and 100 WECs, coordination, power, q-factor, and total farm power output. The dataset was derived from a study published at the GECCO conference and received the prestigious Best Paper award. We want to acknowledge the support of the University of Adelaide Phoenix HPC service in conducting this research. For more details, please refer to the following link: https://dl.acm.org/doi/abs/10.1145/3377930.3390235.",
|
||||||
|
"SUPPORT2": "This dataset comprises 9105 individual critically ill patients across 5 United States medical centers, accessioned throughout 1989-1991 and 1992-1994.\nEach row concerns hospitalized patient records who met the inclusion and exclusion criteria for nine disease categories: acute respiratory failure, chronic obstructive pulmonary disease, congestive heart failure, liver disease, coma, colon cancer, lung cancer, multiple organ system failure with malignancy, and multiple organ system failure with sepsis. The goal is to determine these patients' 2- and 6-month survival rates based on several physiologic, demographics, and disease severity information. \nIt is an important problem because it addresses the growing national concern over patients' loss of control near the end of life. It enables earlier decisions and planning to reduce the frequency of a mechanical, painful, and prolonged dying process."
|
||||||
|
}
|
||||||
|
}
|
||||||
416
30-days-of-python/22_Web_scraping/us_presidents.json
Normal file
416
30-days-of-python/22_Web_scraping/us_presidents.json
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"no.[a]": "1",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "George Washington",
|
||||||
|
"term[14]": "April 30, 1789",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Unaffiliated",
|
||||||
|
"vice president[16]": "1788\u20131789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "2",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "John Adams",
|
||||||
|
"term[14]": "March 4, 1797",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Federalist",
|
||||||
|
"vice president[16]": "1796"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "3",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Thomas Jefferson",
|
||||||
|
"term[14]": "March 4, 1801",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic-",
|
||||||
|
"vice president[16]": "1800"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "4",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "James Madison",
|
||||||
|
"term[14]": "March 4, 1809",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic-",
|
||||||
|
"vice president[16]": "1808"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "5",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "James Monroe",
|
||||||
|
"term[14]": "March 4, 1817",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic-",
|
||||||
|
"vice president[16]": "1816"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "6",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "John Quincy Adams",
|
||||||
|
"term[14]": "March 4, 1825",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic-",
|
||||||
|
"vice president[16]": "1824"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "7",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Andrew Jackson",
|
||||||
|
"term[14]": "March 4, 1829",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1828"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "8",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Martin Van Buren",
|
||||||
|
"term[14]": "March 4, 1837",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1836"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "9",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "William Henry Harrison",
|
||||||
|
"term[14]": "March 4, 1841",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Whig",
|
||||||
|
"vice president[16]": "1840"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "10",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "John Tyler",
|
||||||
|
"term[14]": "April 4, 1841",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Whig",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "11",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "James K. Polk",
|
||||||
|
"term[14]": "March 4, 1845",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1844"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "12",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Zachary Taylor",
|
||||||
|
"term[14]": "March 4, 1849",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Whig",
|
||||||
|
"vice president[16]": "1848"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "13",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Millard Fillmore",
|
||||||
|
"term[14]": "July 9, 1850",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Whig",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "14",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Franklin Pierce",
|
||||||
|
"term[14]": "March 4, 1853",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1852"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "15",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "James Buchanan",
|
||||||
|
"term[14]": "March 4, 1857",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1856"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "16",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Abraham Lincoln",
|
||||||
|
"term[14]": "March 4, 1861",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1860"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "17",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Andrew Johnson",
|
||||||
|
"term[14]": "April 15, 1865",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "National Union",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "18",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Ulysses S. Grant",
|
||||||
|
"term[14]": "March 4, 1869",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1868"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "19",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Rutherford B. Hayes",
|
||||||
|
"term[14]": "March 4, 1877",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1876"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "20",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "James A. Garfield",
|
||||||
|
"term[14]": "March 4, 1881",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1880"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "21",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Chester A. Arthur",
|
||||||
|
"term[14]": "September 19, 1881",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "22",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Grover Cleveland",
|
||||||
|
"term[14]": "March 4, 1885",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1884"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "23",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Benjamin Harrison",
|
||||||
|
"term[14]": "March 4, 1889",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1888"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "24",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Grover Cleveland",
|
||||||
|
"term[14]": "March 4, 1893",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1892"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "25",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "William McKinley",
|
||||||
|
"term[14]": "March 4, 1897",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1896"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "26",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Theodore Roosevelt",
|
||||||
|
"term[14]": "September 14, 1901",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "27",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "William Howard Taft",
|
||||||
|
"term[14]": "March 4, 1909",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1908"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "28",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Woodrow Wilson",
|
||||||
|
"term[14]": "March 4, 1913",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1912"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "29",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Warren G. Harding",
|
||||||
|
"term[14]": "March 4, 1921",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1920"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "30",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Calvin Coolidge",
|
||||||
|
"term[14]": "August 2, 1923",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "31",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Herbert Hoover",
|
||||||
|
"term[14]": "March 4, 1929",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1928"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "32",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Franklin D. Roosevelt",
|
||||||
|
"term[14]": "March 4, 1933",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1932"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "33",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Harry S. Truman",
|
||||||
|
"term[14]": "April 12, 1945",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "34",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Dwight D. Eisenhower",
|
||||||
|
"term[14]": "January 20, 1953",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1952"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "35",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "John F. Kennedy",
|
||||||
|
"term[14]": "January 20, 1961",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1960"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "36",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Lyndon B. Johnson",
|
||||||
|
"term[14]": "November 22, 1963",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "37",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Richard Nixon",
|
||||||
|
"term[14]": "January 20, 1969",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1968"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "38",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Gerald Ford",
|
||||||
|
"term[14]": "August 9, 1974",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "\u2013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "39",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Jimmy Carter",
|
||||||
|
"term[14]": "January 20, 1977",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1976"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "40",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Ronald Reagan",
|
||||||
|
"term[14]": "January 20, 1981",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1980"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "41",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "George H. W. Bush",
|
||||||
|
"term[14]": "January 20, 1989",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "1988"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "42",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Bill Clinton",
|
||||||
|
"term[14]": "January 20, 1993",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "1992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "43",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "George W. Bush",
|
||||||
|
"term[14]": "January 20, 2001",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "2000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "44",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Barack Obama",
|
||||||
|
"term[14]": "January 20, 2009",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "2008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "45",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Donald Trump",
|
||||||
|
"term[14]": "January 20, 2017",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Republican",
|
||||||
|
"vice president[16]": "2016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no.[a]": "46",
|
||||||
|
"portrait": "\n",
|
||||||
|
"name(birth\u2013death)": "Joe Biden",
|
||||||
|
"term[14]": "January 20, 2021",
|
||||||
|
"party[b][15]": "\n",
|
||||||
|
"election": "Democratic",
|
||||||
|
"vice president[16]": "2020"
|
||||||
|
}
|
||||||
|
]
|
||||||
1
30-days-of-python/23_Entorno_virtual/.gitignore
vendored
Normal file
1
30-days-of-python/23_Entorno_virtual/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
venv
|
||||||
@@ -6,4 +6,52 @@ Documento original en inglés: [Virtual Environment](https://github.com/Asabeneh
|
|||||||
|
|
||||||
1. Crea un directorio de proyecto con un entorno virtual basado en el ejemplo dado arriba.
|
1. Crea un directorio de proyecto con un entorno virtual basado en el ejemplo dado arriba.
|
||||||
|
|
||||||
|
Solución:
|
||||||
|
|
||||||
|
Instalar la herramienta virtualenv con el comando:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install virtualenv
|
||||||
|
```
|
||||||
|
|
||||||
|
Crear carpeta para proyecto de Flask:
|
||||||
|
```
|
||||||
|
mkdir flask_project
|
||||||
|
```
|
||||||
|
|
||||||
|
Crear entorno virtual dentro de la carpeta flask_project:
|
||||||
|
```
|
||||||
|
cd flask_project
|
||||||
|
virtualenv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
El comando `virtualenv venv` crea un nuevo entorno virtual llamado `venv` dentro de la carpeta `flask_project`.
|
||||||
|
|
||||||
|
Activar el entorno virtual:
|
||||||
|
|
||||||
|
```
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Después de activar el entorno virtual, deberías ver (venv) al principio de la línea de comandos en la terminal. Esto indica que el entorno virtual está activo. En mi caso tengo el nombre del entorno virtual con zsh en la derecha.
|
||||||
|
|
||||||
|
Instalar Flask en el entorno virtual:
|
||||||
|
```
|
||||||
|
pip install Flask
|
||||||
|
```
|
||||||
|
|
||||||
|
Verificar que se haya instalado correctamente escribiendo el siguiente comando:
|
||||||
|
```
|
||||||
|
pip freeze
|
||||||
|
```
|
||||||
|

|
||||||
|
|
||||||
|
Al terminar se debes desactivar el entorno virtual:
|
||||||
|
```
|
||||||
|
deactivate
|
||||||
|
```
|
||||||
|
|
||||||
|
Incluir la carpeta venv en el archivo [.gitignore](.gitignore) para evitar subirla al repositorio remoto.
|
||||||
|
|
||||||
[<< Day 22](../22_Web_scraping/README.md) | [Day 24 >>](../24_Estadísticas/README.md)
|
[<< Day 22](../22_Web_scraping/README.md) | [Day 24 >>](../24_Estadísticas/README.md)
|
||||||
|
|||||||
BIN
30-days-of-python/23_Entorno_virtual/pic/venv.png
Normal file
BIN
30-days-of-python/23_Entorno_virtual/pic/venv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
358
30-days-of-python/24_Estadísticas/01_stats.py
Normal file
358
30-days-of-python/24_Estadísticas/01_stats.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
"""
|
||||||
|
01_stats.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Info numpy
|
||||||
|
print('numpy version:', np.__version__)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(dir(np))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Creating int numpy arrays
|
||||||
|
|
||||||
|
python_list = [1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
print('Type: ', type(python_list))
|
||||||
|
|
||||||
|
two_dimensional_list = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
|
||||||
|
|
||||||
|
print(two_dimensional_list)
|
||||||
|
|
||||||
|
numpy_array_from_list = np.array(python_list)
|
||||||
|
print(type(numpy_array_from_list))
|
||||||
|
|
||||||
|
print(numpy_array_from_list)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Creating float numpy arrays
|
||||||
|
|
||||||
|
python_list = [1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
numy_array_from_list2 = np.array(python_list, dtype=float)
|
||||||
|
print(numy_array_from_list2)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Creating boolean numpy arrays
|
||||||
|
|
||||||
|
numpy_bool_array = np.array([0, 1, -1, 0, 0], dtype=bool)
|
||||||
|
print(numpy_bool_array)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Creating multidimensional array using numpy
|
||||||
|
|
||||||
|
numpy_two_dimensional_list = np.array(two_dimensional_list)
|
||||||
|
print(type(numpy_two_dimensional_list))
|
||||||
|
print(numpy_two_dimensional_list)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Converting numpy array to list
|
||||||
|
|
||||||
|
np_to_list = numpy_array_from_list.tolist()
|
||||||
|
print(type(np_to_list))
|
||||||
|
print('one dimensional array:', np_to_list)
|
||||||
|
print('two dimensional array: ', numpy_two_dimensional_list.tolist())
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Creating numpy array from tuple
|
||||||
|
|
||||||
|
python_tuple = (1, 2, 3, 4, 5)
|
||||||
|
print(type(python_tuple))
|
||||||
|
print('python_tuple: ', python_tuple)
|
||||||
|
|
||||||
|
numpy_array_from_tuple = np.array(python_tuple)
|
||||||
|
print(type(numpy_array_from_tuple))
|
||||||
|
print('numpy_array_from_tuple: ', numpy_array_from_tuple)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Shape of numpy array
|
||||||
|
nums = np.array([1, 2, 3, 4, 5])
|
||||||
|
print(nums)
|
||||||
|
print('shape of nums: ', nums.shape)
|
||||||
|
print(numpy_two_dimensional_list)
|
||||||
|
print('shape of numpy_two_dimensional_list: ',
|
||||||
|
numpy_two_dimensional_list.shape)
|
||||||
|
three_by_four_array = np.array([[0, 1, 2, 3],
|
||||||
|
[4, 5, 6, 7],
|
||||||
|
[8, 9, 10, 11]])
|
||||||
|
print(three_by_four_array.shape)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Data type of numpy array
|
||||||
|
|
||||||
|
int_lists = [-3, -2, -1, 0, 1, 2, 3]
|
||||||
|
int_array = np.array(int_lists)
|
||||||
|
float_array = np.array(int_lists, dtype=float)
|
||||||
|
|
||||||
|
print(int_array)
|
||||||
|
print(int_array.dtype)
|
||||||
|
print(float_array)
|
||||||
|
print(float_array.dtype)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Size of a numpy array
|
||||||
|
|
||||||
|
numpy_array_from_list = np.array([1, 2, 3, 4, 5])
|
||||||
|
two_dimensional_list = np.array([[0, 1, 2],
|
||||||
|
[3, 4, 5],
|
||||||
|
[6, 7, 8]])
|
||||||
|
|
||||||
|
print('The size:', numpy_array_from_list.size)
|
||||||
|
print('The size:', two_dimensional_list.size)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Mathematical Operation using numpy
|
||||||
|
|
||||||
|
# Addition
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_plus_original = numpy_array_from_list + 10
|
||||||
|
print(ten_plus_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Subtraction
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_minus_original = numpy_array_from_list - 10
|
||||||
|
print(ten_minus_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Multiplication
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_times_original = numpy_array_from_list * 10
|
||||||
|
print(ten_times_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Division
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_times_original = numpy_array_from_list / 10
|
||||||
|
print(ten_times_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Modulus
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_times_original = numpy_array_from_list % 3
|
||||||
|
print(ten_times_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Floor division
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_times_original = numpy_array_from_list // 10
|
||||||
|
print(ten_times_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Exponential
|
||||||
|
print('original array: ', numpy_array_from_list)
|
||||||
|
ten_times_original = numpy_array_from_list ** 2
|
||||||
|
print(ten_times_original)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Int, Float numbers
|
||||||
|
numpy_int_arr = np.array([1, 2, 3, 4])
|
||||||
|
numpy_float_arr = np.array([1.1, 2.0, 3.2])
|
||||||
|
numpy_bool_arr = np.array([-3, -2, 0, 1, 2, 3], dtype='bool')
|
||||||
|
|
||||||
|
print(numpy_int_arr.dtype)
|
||||||
|
print(numpy_float_arr.dtype)
|
||||||
|
print(numpy_bool_arr.dtype)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Converting types
|
||||||
|
|
||||||
|
# int to float
|
||||||
|
numpy_int_arr = np.array([1, 2, 3, 4], dtype='float')
|
||||||
|
print(numpy_int_arr)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# float to int
|
||||||
|
numpy_int_arr = np.array(numpy_int_arr, dtype='int')
|
||||||
|
print(numpy_int_arr)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# float to bool
|
||||||
|
numpy_int_arr = np.array([-3, -2, 0, 1, 2, 3], dtype='bool')
|
||||||
|
print(numpy_int_arr)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# int to str
|
||||||
|
|
||||||
|
numpy_int_arr = np.array([1, 2, 3, 4], dtype='str')
|
||||||
|
print(numpy_int_arr)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Multi-dimensional Arrays
|
||||||
|
|
||||||
|
# 2 Dimension Array
|
||||||
|
two_dimension_array = np.array([
|
||||||
|
(1, 2, 3),
|
||||||
|
(4, 5, 6),
|
||||||
|
(7, 8, 9)
|
||||||
|
])
|
||||||
|
print(type(two_dimension_array))
|
||||||
|
print(two_dimension_array)
|
||||||
|
print('Shape: ', two_dimension_array.shape)
|
||||||
|
print('Size:', two_dimension_array.size)
|
||||||
|
print('Data type:', two_dimension_array.dtype)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Getting items from a numpy array
|
||||||
|
|
||||||
|
# 2 Dimension Array
|
||||||
|
two_dimension_array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||||
|
first_row = two_dimension_array[0]
|
||||||
|
second_row = two_dimension_array[1]
|
||||||
|
third_row = two_dimension_array[2]
|
||||||
|
print('First row:', first_row)
|
||||||
|
print('Second row:', second_row)
|
||||||
|
print('Third row: ', third_row)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
first_column = two_dimension_array[:, 0]
|
||||||
|
second_column = two_dimension_array[:, 1]
|
||||||
|
third_column = two_dimension_array[:, 2]
|
||||||
|
print('First column:', first_column)
|
||||||
|
print('Second column:', second_column)
|
||||||
|
print('Third column: ', third_column)
|
||||||
|
print(two_dimension_array)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Slicing Numpy array
|
||||||
|
|
||||||
|
first_two_rows_and_columns = two_dimension_array[0:2, 0:2]
|
||||||
|
print(first_two_rows_and_columns)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# How to reverse the rows and the whole array?
|
||||||
|
|
||||||
|
print(two_dimension_array[::])
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Reverse the row and column positions
|
||||||
|
|
||||||
|
print(two_dimension_array[::-1, ::-1])
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# How to represent missing values ?
|
||||||
|
print(two_dimension_array)
|
||||||
|
two_dimension_array[1, 1] = 55
|
||||||
|
two_dimension_array[1, 2] = 44
|
||||||
|
print(two_dimension_array)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Numpy Zeroes
|
||||||
|
# numpy.zeros(shape, dtype=float, order='C')
|
||||||
|
numpy_zeroes = np.zeros((3, 3), dtype=int, order='C')
|
||||||
|
print(numpy_zeroes)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# Numpy Zeroes
|
||||||
|
numpy_ones = np.ones((3, 3), dtype=int, order='C')
|
||||||
|
print(numpy_ones)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
twoes = numpy_ones * 2
|
||||||
|
|
||||||
|
print(twoes)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Reshape
|
||||||
|
# numpy.reshape(), numpy.flatten()
|
||||||
|
first_shape = np.array([(1, 2, 3), (4, 5, 6)])
|
||||||
|
print(first_shape)
|
||||||
|
reshaped = first_shape.reshape(3, 2)
|
||||||
|
print(reshaped)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
flattened = reshaped.flatten()
|
||||||
|
print(flattened)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Horitzontal Stack
|
||||||
|
np_list_one = np.array([1, 2, 3])
|
||||||
|
np_list_two = np.array([4, 5, 6])
|
||||||
|
|
||||||
|
print(np_list_one + np_list_two)
|
||||||
|
|
||||||
|
print('Horizontal Append:', np.hstack((np_list_one, np_list_two)))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Vertical Stack
|
||||||
|
print('Vertical Append:', np.vstack((np_list_one, np_list_two)))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generating Random Numbers
|
||||||
|
|
||||||
|
# Generate a random float number
|
||||||
|
random_float = np.random.random()
|
||||||
|
print(random_float)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generate a random float number
|
||||||
|
random_floats = np.random.random(5)
|
||||||
|
print(random_floats)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generating a random integers between 0 and 10
|
||||||
|
|
||||||
|
random_int = np.random.randint(0, 11)
|
||||||
|
print(random_int)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generating a random integers between 2 and 11, and creating a one row array
|
||||||
|
random_int = np.random.randint(2, 10, size=4)
|
||||||
|
print(random_int)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generating a random integers between 0 and 10
|
||||||
|
random_int = np.random.randint(2, 10, size=(3, 3))
|
||||||
|
print(random_int)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generationg random numbers
|
||||||
|
|
||||||
|
# np.random.normal(mu, sigma, size)
|
||||||
|
normal_array = np.random.normal(79, 15, 80)
|
||||||
|
print(normal_array)
|
||||||
|
|
||||||
|
print()
|
||||||
274
30-days-of-python/24_Estadísticas/02_stats.py
Normal file
274
30-days-of-python/24_Estadísticas/02_stats.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
"""
|
||||||
|
02_stats.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import seaborn as sns
|
||||||
|
|
||||||
|
from scipy import stats
|
||||||
|
|
||||||
|
# Numpy and Statistics
|
||||||
|
|
||||||
|
# Matrix in numpy
|
||||||
|
|
||||||
|
four_by_four_matrix = np.matrix(np.ones((4, 4), dtype=float))
|
||||||
|
print(four_by_four_matrix)
|
||||||
|
|
||||||
|
np.asarray(four_by_four_matrix)[2] = 2
|
||||||
|
print(four_by_four_matrix)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Numpy numpy.arange()
|
||||||
|
|
||||||
|
# creating list using range(starting, stop, step)
|
||||||
|
lst = range(0, 11, 2)
|
||||||
|
print(lst)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
for l in lst:
|
||||||
|
print(l)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Similar to range arange numpy.arange(start, stop, step)
|
||||||
|
whole_numbers = np.arange(0, 20, 1)
|
||||||
|
print(whole_numbers)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
natural_numbers = np.arange(1, 20, 1)
|
||||||
|
print(natural_numbers)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
odd_numbers = np.arange(1, 20, 2)
|
||||||
|
print(odd_numbers)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
even_numbers = np.arange(2, 20, 2)
|
||||||
|
print(even_numbers)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Creating sequence of numbers using linspace
|
||||||
|
|
||||||
|
# numpy.linspace()
|
||||||
|
# numpy.logspace() in Python with Example
|
||||||
|
# For instance, it can be used to create 10 values from 1 to 5 evenly spaced.
|
||||||
|
print(np.linspace(1.0, 5.0, num=10))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# not to include the last value in the interval
|
||||||
|
print(np.linspace(1.0, 5.0, num=5, endpoint=False))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# LogSpace
|
||||||
|
# LogSpace returns even spaced numbers on a log scale. Logspace has the same parameters as np.linspace.
|
||||||
|
|
||||||
|
# Syntax:
|
||||||
|
|
||||||
|
# numpy.logspace(start, stop, num, endpoint)
|
||||||
|
|
||||||
|
print(np.logspace(2, 4.0, num=4))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# to check the size of an array
|
||||||
|
x = np.array([1, 2, 3], dtype=np.complex128)
|
||||||
|
print(x)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(x.itemsize)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# indexing and Slicing NumPy Arrays in Python
|
||||||
|
np_list = np.array([(1, 2, 3), (4, 5, 6)])
|
||||||
|
print(np_list)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
print('First row: ', np_list[0])
|
||||||
|
print('Second row: ', np_list[1])
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
print('First column: ', np_list[:, 0])
|
||||||
|
print('Second column: ', np_list[:, 1])
|
||||||
|
print('Third column: ', np_list[:, 2])
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# NumPy Statistical Functions with Example
|
||||||
|
|
||||||
|
np_normal_dis = np.random.normal(5, 0.5, 100)
|
||||||
|
print(np_normal_dis)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# min, max, mean, median, sd
|
||||||
|
|
||||||
|
two_dimension_array = np.array([
|
||||||
|
(1, 2, 3),
|
||||||
|
(4, 5, 6),
|
||||||
|
(7, 8, 9)
|
||||||
|
])
|
||||||
|
print('min: ', two_dimension_array.min())
|
||||||
|
print('max: ', two_dimension_array.max())
|
||||||
|
print('mean: ', two_dimension_array.mean())
|
||||||
|
# print('median: ', two_dimension_array.median())
|
||||||
|
print('sd: ', two_dimension_array.std())
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(two_dimension_array)
|
||||||
|
print('Column with minimum: ', np.amin(two_dimension_array, axis=0))
|
||||||
|
print('Column with maximum: ', np.amax(two_dimension_array, axis=0))
|
||||||
|
print('=== Row ==')
|
||||||
|
print('Row with minimum: ', np.amin(two_dimension_array, axis=1))
|
||||||
|
print('Row with maximum: ', np.amax(two_dimension_array, axis=1))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# How to create repeating sequences?
|
||||||
|
|
||||||
|
a = [1, 2, 3]
|
||||||
|
|
||||||
|
# Repeat whole of 'a' two times
|
||||||
|
print('Tile: ', np.tile(a, 2))
|
||||||
|
|
||||||
|
# Repeat each element of 'a' two times
|
||||||
|
print('Repeat: ', np.repeat(a, 2))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# How to generate random numbers?
|
||||||
|
|
||||||
|
# One random number between [0,1)
|
||||||
|
one_random_num = np.random.random()
|
||||||
|
one_random_in = np.random
|
||||||
|
print(one_random_num)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Random numbers between [0,1) of shape 2,3
|
||||||
|
r = np.random.random(size=[2, 3])
|
||||||
|
print(r)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(np.random.choice(['a', 'e', 'i', 'o', 'u'], size=10))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Random numbers between [0, 1] of shape 2, 2
|
||||||
|
rand = np.random.rand(2, 2)
|
||||||
|
print(rand)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
rand2 = np.random.randn(2, 2)
|
||||||
|
print(rand2)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Random integers between [0, 10) of shape 2,5
|
||||||
|
rand_int = np.random.randint(0, 10, size=[5, 3])
|
||||||
|
print(rand_int)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# mean, standard deviation, number of samples
|
||||||
|
np_normal_dis = np.random.normal(5, 0.5, 1000)
|
||||||
|
np_normal_dis
|
||||||
|
# min, max, mean, median, sd
|
||||||
|
print('min: ', np.min(np_normal_dis))
|
||||||
|
print('max: ', np.max(np_normal_dis))
|
||||||
|
print('mean: ', np.mean(np_normal_dis))
|
||||||
|
print('median: ', np.median(np_normal_dis))
|
||||||
|
print('mode: ', stats.mode(np_normal_dis))
|
||||||
|
print('sd: ', np.std(np_normal_dis))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
plt.hist(np_normal_dis, color="grey", bins=21)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# Linear algebra
|
||||||
|
|
||||||
|
# Dot product: product of two arrays
|
||||||
|
f = np.array([1, 2, 3])
|
||||||
|
g = np.array([4, 5, 3])
|
||||||
|
# 1*4+2*5 + 3*6
|
||||||
|
dot_product = np.dot(f, g)
|
||||||
|
print(dot_product)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Matmul: matruc product of two arrays
|
||||||
|
h = [[1, 2], [3, 4]]
|
||||||
|
i = [[5, 6], [7, 8]]
|
||||||
|
# 1*5+2*7 = 19
|
||||||
|
matmul = np.matmul(h, i)
|
||||||
|
print(matmul)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Determinant 2*2 matrix
|
||||||
|
# 5*8-7*6np.linalg.det(i)
|
||||||
|
matri = np.linalg.det(i)
|
||||||
|
print(matri)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
Z = np.zeros((8, 8))
|
||||||
|
Z[1::2, ::2] = 1
|
||||||
|
Z[::2, 1::2] = 1
|
||||||
|
print(Z)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
new_list = [x + 2 for x in range(0, 11)]
|
||||||
|
print(new_list)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
np_arr = np.array(range(0, 11))
|
||||||
|
np_arr + 2
|
||||||
|
print(np_arr)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
temp = np.array([1, 2, 3, 4, 5])
|
||||||
|
pressure = temp * 2 + 5
|
||||||
|
print(pressure)
|
||||||
|
|
||||||
|
plt.plot(temp, pressure)
|
||||||
|
plt.xlabel('Temperature in oC')
|
||||||
|
plt.ylabel('Pressure in atm')
|
||||||
|
plt.title('Temperature vs Pressure')
|
||||||
|
plt.xticks(np.arange(0, 6, step=0.5))
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
mu = 28
|
||||||
|
sigma = 15
|
||||||
|
samples = 100000
|
||||||
|
|
||||||
|
x = np.random.normal(mu, sigma, samples)
|
||||||
|
ax = sns.distplot(x)
|
||||||
|
ax.set(xlabel="x", ylabel='y')
|
||||||
|
plt.show()
|
||||||
@@ -6,4 +6,8 @@ Documento original en inglés: [statistics](https://github.com/Asabeneh/30-Days-
|
|||||||
|
|
||||||
1. Repite todos los [ejemplos](https://github.com/Asabeneh/30-Days-Of-Python/blob/master/24_Day_Statistics/24_statistics.md)
|
1. Repite todos los [ejemplos](https://github.com/Asabeneh/30-Days-Of-Python/blob/master/24_Day_Statistics/24_statistics.md)
|
||||||
|
|
||||||
|
[Solución 01](01_stats.py)
|
||||||
|
|
||||||
|
[Solución 02](02_stats.py)
|
||||||
|
|
||||||
[<< Day 23](../23_Entorno_virtual/README.md) | [Day 25 >>](../25_Pandas/README.md)
|
[<< Day 23](../23_Entorno_virtual/README.md) | [Day 25 >>](../25_Pandas/README.md)
|
||||||
|
|||||||
43
30-days-of-python/25_Pandas/01_pandas.py
Normal file
43
30-days-of-python/25_Pandas/01_pandas.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
01_pandas.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# 1. Lee el archivo hacker_news.csv del directorio de datos.
|
||||||
|
df = pd.read_csv('hacker_news.csv')
|
||||||
|
|
||||||
|
# 2. Obtén las primeras cinco filas.
|
||||||
|
print(df.head())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 3. Obtén las últimas cinco filas.
|
||||||
|
print(df.tail())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 4. Obtén la columna de títulos como una serie de pandas.
|
||||||
|
titles = df['title']
|
||||||
|
print(titles)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 5. Cuenta el número de filas y columnas.
|
||||||
|
print('Número de filas:', len(df))
|
||||||
|
print('Número de columnas:', len(df.columns))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Filtra los títulos que contengan "python".
|
||||||
|
python_titles = df[df['title'].str.contains('python', case=False)]
|
||||||
|
print(python_titles)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Filtra los títulos que contengan "JavaScript".
|
||||||
|
js_titles = df[df['title'].str.contains('JavaScript', case=False)]
|
||||||
|
print(js_titles)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Explora los datos y dales sentido.
|
||||||
|
# Puedes utilizar métodos como describe(), info(), value_counts(), etc. para explorar los datos y obtener estadísticas descriptivas.
|
||||||
|
print(df.describe())
|
||||||
|
print(df.info())
|
||||||
|
print(df['title'].value_counts())
|
||||||
|
print()
|
||||||
@@ -17,4 +17,6 @@ Documento original en inglés: [Pandas](https://github.com/Asabeneh/30-Days-Of-P
|
|||||||
- Filtra los títulos que contengan "JavaScript".
|
- Filtra los títulos que contengan "JavaScript".
|
||||||
- Explora los datos y dales sentido.
|
- Explora los datos y dales sentido.
|
||||||
|
|
||||||
|
[Solución](01_pandas.py)
|
||||||
|
|
||||||
[<< Day 24](../24_Estadísticas/README.md) | [Day 26 >>](../26_Desarrollo_web_en_Python/README.md)
|
[<< Day 24](../24_Estadísticas/README.md) | [Day 26 >>](../26_Desarrollo_web_en_Python/README.md)
|
||||||
|
|||||||
20100
30-days-of-python/25_Pandas/hacker_news.csv
Normal file
20100
30-days-of-python/25_Pandas/hacker_news.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,4 +6,6 @@ Documento original en inglés: [Python web](https://github.com/Asabeneh/30-Days-
|
|||||||
|
|
||||||
1. Construirás esta [aplicación](https://thirtydaysofpython-v1-final.herokuapp.com/). Solo queda la parte del analizador de texto.
|
1. Construirás esta [aplicación](https://thirtydaysofpython-v1-final.herokuapp.com/). Solo queda la parte del analizador de texto.
|
||||||
|
|
||||||
|
[Solución](./web/)
|
||||||
|
|
||||||
[<< Day 25](../25_Pandas/README.md) | [Day 27 >>](../27_Python_con_MongoDB/README.md)
|
[<< Day 25](../25_Pandas/README.md) | [Day 27 >>](../27_Python_con_MongoDB/README.md)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
web: python app.py
|
||||||
44
30-days-of-python/26_Desarrollo_web_en_Python/web/app.py
Normal file
44
30-days-of-python/26_Desarrollo_web_en_Python/web/app.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
app.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def home():
|
||||||
|
techs = ['HTML', 'CSS', 'Flask', 'Python']
|
||||||
|
name = '30 Days Of Python Programming'
|
||||||
|
return render_template('home.html', techs=techs, name=name, title='Home')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/about')
|
||||||
|
def about():
|
||||||
|
name = '30 Days Of Python Programming'
|
||||||
|
return render_template('about.html', name=name, title='About Us')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/result')
|
||||||
|
def result():
|
||||||
|
return render_template('result.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/post', methods=['GET', 'POST'])
|
||||||
|
def post():
|
||||||
|
name = 'Text Analyzer'
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('post.html', name=name, title=name)
|
||||||
|
if request.method == 'POST':
|
||||||
|
content = request.form['content']
|
||||||
|
print(content)
|
||||||
|
return redirect(url_for('result'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
blinker==1.7.0
|
||||||
|
click==8.1.7
|
||||||
|
Flask==3.0.2
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
Jinja2==3.1.3
|
||||||
|
MarkupSafe==2.1.5
|
||||||
|
Werkzeug==3.0.1
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/* === GENERAL === */
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === css variables === */
|
||||||
|
:root {
|
||||||
|
--header-bg-color: #4a7799;
|
||||||
|
--textarea-bg-color: rgb(250, 246, 246);
|
||||||
|
--body-bg-color: rgb(210, 214, 210);
|
||||||
|
--nav-link-color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === body style === */
|
||||||
|
body {
|
||||||
|
background: var(--body-bg-color);
|
||||||
|
margin: auto;
|
||||||
|
line-height: 1.75;
|
||||||
|
font-weight: 900;
|
||||||
|
word-spacing: 1.5px;
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === header style === */
|
||||||
|
header {
|
||||||
|
background: var(--header-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === title and subtitle style === */
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-family: Nunito;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === header menu style === */
|
||||||
|
|
||||||
|
.menu-container {
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
color: rgb(221, 215, 215);
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-lists {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: var(--nav-link-color);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === paragraph text style === */
|
||||||
|
p {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === main style === */
|
||||||
|
main {
|
||||||
|
width: 90%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === container div inside main style === */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: rgb(210, 214, 210);
|
||||||
|
padding: 20px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-lists {
|
||||||
|
margin: 10px auto;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === button style === */
|
||||||
|
.btn {
|
||||||
|
width: 150px;
|
||||||
|
height: 50px;
|
||||||
|
background: var(--header-bg-color);
|
||||||
|
color: var(--nav-link-color);
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 1px solid var(--header-bg-color);
|
||||||
|
font-family: Lato;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
outline: 2px solid #2a70a5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === textarea style === */
|
||||||
|
textarea {
|
||||||
|
width: 65%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 15px;
|
||||||
|
outline: 2px solid rgba(207, 203, 203, 0.25);
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: Lato;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
border: none;
|
||||||
|
outline: 2px solid rgba(74, 119, 153, 0.45);
|
||||||
|
background: var(--textarea-bg-color);
|
||||||
|
font-size: 18px;
|
||||||
|
caret-color: var(--header-bg-color);
|
||||||
|
font-family: Lato;
|
||||||
|
font-weight: 300;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: .35em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr,
|
||||||
|
tbody td {
|
||||||
|
padding: .625em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr th {
|
||||||
|
background: var(--header-bg-color);
|
||||||
|
font-size: .85em;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 15px;
|
||||||
|
color: var(--nav-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === responsiveness === */
|
||||||
|
@media (max-width:600px) {
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-lists {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'layout.html' %} {% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Sobre esto</h1>
|
||||||
|
<iframe src="https://giphy.com/embed/eH9sawQbajAQM" width="480" height="120"
|
||||||
|
frameBorder="0" class="giphy-embed" allowFullScreen></iframe>
|
||||||
|
<p>
|
||||||
|
Poco que decir, solo que esto es una web hecha con python y flask.
|
||||||
|
Es un ejercicio que se ha extraído del <a
|
||||||
|
href="https://github.com/Asabeneh/30-Days-Of-Python"> repositorio
|
||||||
|
github de 30DaysOfPython</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'layout.html' %} {% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Welcome to {{name}}</h1>
|
||||||
|
<div>
|
||||||
|
<img src="https://c.tenor.com/Zdpc10JrZrIAAAAC/tenor.gif"
|
||||||
|
alt="python" />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
This application clean texts and analyse the number of word, characters
|
||||||
|
and
|
||||||
|
most frequent words in the text. Check it out by click text analyzer at
|
||||||
|
the
|
||||||
|
menu. You need the following technologies to build this web application:
|
||||||
|
</p>
|
||||||
|
<ul class="tech-lists">
|
||||||
|
{% for tech in techs %}
|
||||||
|
<li class="tech">{{tech}}</li>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Lato:300,400|Nunito:300,400|Raleway:300,400,500&display=swap"
|
||||||
|
rel="stylesheet" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='css/main.css') }}" />
|
||||||
|
{% if title %}
|
||||||
|
<title>30 Days of Python - {{ title}}</title>
|
||||||
|
{% else %}
|
||||||
|
<title>30 Days of Python</title>
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="menu-container">
|
||||||
|
<div>
|
||||||
|
<a class="brand-name nav-link" href="/">30DaysOfPython</a>
|
||||||
|
</div>
|
||||||
|
<ul class="nav-lists">
|
||||||
|
<li class="nav-list">
|
||||||
|
<a class="nav-link active" href="{{ url_for('home') }}">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-list">
|
||||||
|
<a class="nav-link active" href="{{ url_for('about') }}">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-list">
|
||||||
|
<a class="nav-link active" href="{{ url_for('post') }}">Text
|
||||||
|
Analyzer</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %} {% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'layout.html' %} {% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Text Analyzer</h1>
|
||||||
|
<form action="localhost:5000/post"
|
||||||
|
method="POST">
|
||||||
|
<div>
|
||||||
|
<textarea rows="25" name="content" autofocus></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="submit" class="btn" value="Process Text" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
Conection
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
# Create a new client and connect to the server
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
# Send a ping to confirm a successful connection
|
||||||
|
try:
|
||||||
|
client.admin.command('ping')
|
||||||
|
print("Se envió un ping a su deploy. ¡Se ha conectado correctamente a MongoDB!")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Conectar la aplicación flask a la base de datos MongoDB
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
print(client.list_database_names())
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Crear una base de datos y una colección
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
# Creating database
|
||||||
|
db = client.pruebas_mongodb
|
||||||
|
|
||||||
|
# Creating students collection and inserting a document
|
||||||
|
db.students.insert_one({
|
||||||
|
'name': 'manuel',
|
||||||
|
'country': 'Angola',
|
||||||
|
'city': 'Soria',
|
||||||
|
'age': 40
|
||||||
|
})
|
||||||
|
|
||||||
|
print(client.list_database_names())
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Insertar varios documentos en una colección
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
db = client.pruebas_mongodb
|
||||||
|
|
||||||
|
students = [
|
||||||
|
{'name': 'David', 'country': 'UK', 'city': 'London', 'age': 34},
|
||||||
|
{'name': 'John', 'country': 'Sweden', 'city': 'Stockholm', 'age': 28},
|
||||||
|
{'name': 'Sami', 'country': 'Finland', 'city': 'Helsinki', 'age': 25},
|
||||||
|
]
|
||||||
|
|
||||||
|
for student in students:
|
||||||
|
db.students.insert_one(student)
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for deployment we use the environ
|
||||||
|
# to make it work for both production and development
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
55
30-days-of-python/27_Python_con_MongoDB/05_find.py
Normal file
55
30-days-of-python/27_Python_con_MongoDB/05_find.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
Buscar en la base de datos
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
db = client.pruebas_mongodb
|
||||||
|
|
||||||
|
student = db.students.find_one({'name': 'David'})
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
student_two = db.students.find_one(
|
||||||
|
{'_id': ObjectId('652472bfccdbb81a3f7473e5')})
|
||||||
|
print(student_two)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
students = db.students.find()
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
students = db.students.find({}, {"_id": 0, "name": 1, "country": 1})
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for deployment we use the environ
|
||||||
|
# to make it work for both production and development
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
70
30-days-of-python/27_Python_con_MongoDB/06_find_query.py
Normal file
70
30-days-of-python/27_Python_con_MongoDB/06_find_query.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Buscar en la base de datos con query
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
db = client.pruebas_mongodb
|
||||||
|
|
||||||
|
query = {
|
||||||
|
"country": "Finland"
|
||||||
|
}
|
||||||
|
students = db.students.find(query)
|
||||||
|
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
query = {
|
||||||
|
"city": "Helsinki"
|
||||||
|
}
|
||||||
|
students = db.students.find(query)
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
query = {
|
||||||
|
"country": "Finland",
|
||||||
|
"city": "Helsinki"
|
||||||
|
}
|
||||||
|
students = db.students.find(query)
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
query = {"age": {"$gt": 30}}
|
||||||
|
students = db.students.find(query)
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for deployment we use the environ
|
||||||
|
# to make it work for both production and development
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Buscar en la base de datos - más opciones
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
db = client.pruebas_mongodb
|
||||||
|
|
||||||
|
db.students.find().limit(3)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
students = db.students.find().sort('name')
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
students = db.students.find().sort('name', -1)
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
students = db.students.find().sort('age')
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
students = db.students.find().sort('age', -1)
|
||||||
|
for student in students:
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
print("-"*30)
|
||||||
|
|
||||||
|
# New value
|
||||||
|
|
||||||
|
query = {'age': 40}
|
||||||
|
new_value = {'$set': {'age': 38}}
|
||||||
|
|
||||||
|
db.students.update_one(query, new_value)
|
||||||
|
# lets check the result if the age is modified
|
||||||
|
for student in db.students.find():
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for deployment we use the environ
|
||||||
|
# to make it work for both production and development
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
46
30-days-of-python/27_Python_con_MongoDB/08_delete_doc.py
Normal file
46
30-days-of-python/27_Python_con_MongoDB/08_delete_doc.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Buscar en la base de datos con query
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pymongo.mongo_client import MongoClient
|
||||||
|
from pymongo.server_api import ServerApi
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
# variables .env
|
||||||
|
mongopass = os.getenv('mongopass')
|
||||||
|
mongouser = os.getenv('mongouser')
|
||||||
|
mongoname = os.getenv('mongoname')
|
||||||
|
|
||||||
|
uri = "mongodb+srv://" + \
|
||||||
|
mongouser + ":" + \
|
||||||
|
mongopass + "@" + \
|
||||||
|
mongoname + ".m697hfm.mongodb.net/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
client = MongoClient(uri, server_api=ServerApi('1'))
|
||||||
|
|
||||||
|
db = client.pruebas_mongodb
|
||||||
|
|
||||||
|
# Delete one document
|
||||||
|
query = {'name': 'John'}
|
||||||
|
db.students.delete_one(query)
|
||||||
|
|
||||||
|
# Delete many documents
|
||||||
|
for student in db.students.find():
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
for student in db.students.find():
|
||||||
|
print(student)
|
||||||
|
|
||||||
|
# Drop collection
|
||||||
|
db.students.drop()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for deployment we use the environ
|
||||||
|
# to make it work for both production and development
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
@@ -6,4 +6,15 @@ Documento original en inglés: [Python with MondoDB](https://github.com/Asabeneh
|
|||||||
|
|
||||||
Repasar [ejemplos](https://github.com/Asabeneh/30-Days-Of-Python/blob/master/27_Day_Python_with_mongodb/27_python_with_mongodb.md)
|
Repasar [ejemplos](https://github.com/Asabeneh/30-Days-Of-Python/blob/master/27_Day_Python_with_mongodb/27_python_with_mongodb.md)
|
||||||
|
|
||||||
|
| Solución |
|
||||||
|
| ---------------------------------------------------------------------- |
|
||||||
|
| [01_proof_conection_mongo.py](01_proof_conection_mongo.py) |
|
||||||
|
| [02_connect_flask_app.py](02_connect_flask_app.py) |
|
||||||
|
| [03_create_db_collection.py](03_create_db_collection.py) |
|
||||||
|
| [04_Insert_many_docs_collection.py](04_Insert_many_docs_collection.py) |
|
||||||
|
| [05_find.py](05_find.py) |
|
||||||
|
| [06_find_query.py](06_find_query.py) |
|
||||||
|
| [07_find_more_opcions.py](07_find_more_opcions.py) |
|
||||||
|
| [08_delete_doc.py](08_delete_doc.py) |
|
||||||
|
|
||||||
[<< Day 26](../26_Desarrollo_web_en_Python/README.md) | [Day 28 >>](../28_API/README.md)
|
[<< Day 26](../26_Desarrollo_web_en_Python/README.md) | [Day 28 >>](../28_API/README.md)
|
||||||
|
|||||||
@@ -4,6 +4,115 @@ Documento original en inglés: [API](https://github.com/Asabeneh/30-Days-Of-Pyth
|
|||||||
|
|
||||||
## Ejercicios
|
## Ejercicios
|
||||||
|
|
||||||
1. Lee sobre API y HTTP.
|
1. Lee sobre API y HTTP:
|
||||||
|
|
||||||
|
### Interfaz de programación de aplicaciones (API)
|
||||||
|
|
||||||
|
#### API
|
||||||
|
|
||||||
|
El tipo de API que vamos a ver son las API Web. Son las interfaces definidas a través de las cuales se producen las interacciones entre una empresa y las aplicaciones que utilizan sus activos, que también es un Acuerdo de Nivel de Servicio (SLA) para especificar el proveedor funcional y exponer la ruta de servicio o URL para los usuarios de la API.
|
||||||
|
|
||||||
|
En el contexto del desarrollo web, una API se define como un conjunto de especificaciones, como los mensajes de solicitud del Protocolo de Transferencia de Hipertexto (HTTP), junto con una definición de la estructura de los mensajes de respuesta, normalmente en un formato XML o de Notación de Objetos JavaScript (JSON).
|
||||||
|
|
||||||
|
Las API web se han ido alejando de los servicios web basados en el Protocolo Simple de Acceso a Objetos (SOAP) y la arquitectura orientada a servicios (SOA) para acercarse más directamente a los recursos web de estilo de transferencia de estado representacional (REST).
|
||||||
|
|
||||||
|
Los servicios de medios sociales, las API web han permitido a las comunidades web compartir contenidos y datos entre comunidades y distintas plataformas.
|
||||||
|
|
||||||
|
Gracias a las API, los contenidos creados en un lugar pueden publicarse y actualizarse dinámicamente en varios sitios de la web.
|
||||||
|
|
||||||
|
Por ejemplo, la API REST de Twitter permite a los desarrolladores acceder a los datos básicos de Twitter y la API de búsqueda proporciona métodos para que los desarrolladores interactúen con los datos de búsqueda y tendencias de Twitter.
|
||||||
|
|
||||||
|
Muchas aplicaciones proporcionan puntos finales de API. Algunos ejemplos de API son la [API de países](https://restcountries.eu/rest/v2/all) o la [API de razas de gatos](https://api.thecatapi.com/v1/breeds).
|
||||||
|
|
||||||
|
Vamos a ver una API RESTful que utiliza métodos de solicitud HTTP para GET, PUT, POST y DELETE de datos.
|
||||||
|
|
||||||
|
#### Creación de API
|
||||||
|
|
||||||
|
RESTful API es una interfaz de programación de aplicaciones (API) que utiliza peticiones HTTP para GET, PUT, POST y DELETE de datos. En las secciones anteriores, hemos aprendido sobre python, flask y mongoDB. Utilizaremos los conocimientos adquiridos para desarrollar una API RESTful utilizando Python flask y la base de datos mongoDB. Toda aplicación que tenga operaciones CRUD (Create, Read, Update, Delete) tiene una API para crear datos, obtener datos, actualizar datos o borrar datos de una base de datos.
|
||||||
|
|
||||||
|
Para construir una API, es bueno entender el protocolo HTTP y el ciclo de petición y respuesta HTTP.
|
||||||
|
|
||||||
|
##### HTTP(Protocolo de transferencia de hipertexto)
|
||||||
|
|
||||||
|
HTTP es un protocolo de comunicación establecido entre un cliente y un servidor. Un cliente en este caso es un navegador y el servidor es el lugar donde se accede a los datos. HTTP es un protocolo de red utilizado para entregar recursos que pueden ser archivos en la World Wide Web, ya sean archivos HTML, archivos de imagen, resultados de consultas, scripts u otros tipos de archivos.
|
||||||
|
|
||||||
|
Un navegador es un cliente HTTP porque envía peticiones a un servidor HTTP (servidor Web), que a su vez envía respuestas al cliente.
|
||||||
|
|
||||||
|
##### Estructura de HTTP
|
||||||
|
|
||||||
|
HTTP utiliza el modelo cliente-servidor. Un cliente HTTP abre una conexión y envía un mensaje de solicitud a un servidor HTTP y el servidor HTTP devuelve un mensaje de respuesta que contiene los recursos solicitados. Cuando finaliza el ciclo de respuesta a la solicitud, el servidor cierra la conexión.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
El formato de los mensajes de solicitud y respuesta es similar. Ambos tipos de mensajes tienen
|
||||||
|
|
||||||
|
- una línea inicial,
|
||||||
|
- cero o más líneas de encabezamiento,
|
||||||
|
- una línea en blanco (es decir, un CRLF solo), y
|
||||||
|
- un cuerpo de mensaje opcional (por ejemplo, un archivo, o datos de consulta, o salida de consulta).
|
||||||
|
|
||||||
|
Veamos un ejemplo de mensajes de solicitud y respuesta navegando por este [sitio](https://thirtydaysofpython-v1-final.herokuapp.com/). Este sitio ha sido desplegado en Heroku free dyno y en algunos meses puede no funcionar debido a la alta solicitud. Apoyar este trabajo para que el servidor funcione todo el tiempo.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
#### Línea de solicitud inicial (línea de estado)
|
||||||
|
|
||||||
|
La línea de solicitud inicial es diferente de la respuesta. Una línea de petición tiene tres partes, separadas por espacios:
|
||||||
|
|
||||||
|
- nombre del método (GET, POST, HEAD)
|
||||||
|
- ruta del recurso solicitado,
|
||||||
|
- la versión de HTTP utilizada. ej. GET / HTTP/1.1
|
||||||
|
|
||||||
|
GET es el HTTP más común que ayuda a obtener o leer recursos y POST es un método de solicitud común para crear recursos.
|
||||||
|
Línea de respuesta inicial (línea de estado)
|
||||||
|
|
||||||
|
La línea de respuesta inicial, llamada línea de estado, también tiene tres partes separadas por espacios:
|
||||||
|
|
||||||
|
- Versión HTTP
|
||||||
|
- Código de estado de la respuesta que da el resultado de la petición, y una razón que describe el código de estado. Ejemplos de líneas de estado son: HTTP/1.0 200 OK o HTTP/1.0 404 No encontrado Notas:
|
||||||
|
|
||||||
|
Los códigos de estado más comunes son: 200 OK: La solicitud se ha realizado correctamente y el recurso resultante (por ejemplo, un archivo o la salida de un script) se devuelve en el cuerpo del mensaje. 500 Error de servidor Puede encontrar una lista completa de códigos de estado HTTP aquí. También se puede encontrar aquí.
|
||||||
|
|
||||||
|
##### Campos de cabecera
|
||||||
|
|
||||||
|
Como se ve en la captura de pantalla anterior, las líneas de cabecera proporcionan información sobre la solicitud o la respuesta, o sobre el objeto enviado en el cuerpo del mensaje.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET / HTTP/1.1
|
||||||
|
Host: thirtydaysofpython-v1-final.herokuapp.com
|
||||||
|
Connection: keep-alive
|
||||||
|
Pragma: no-cache
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Upgrade-Insecure-Requests: 1
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36
|
||||||
|
Sec-Fetch-User: ?1
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||||
|
Sec-Fetch-Site: same-origin
|
||||||
|
Sec-Fetch-Mode: navigate
|
||||||
|
Referer: https://thirtydaysofpython-v1-final.herokuapp.com/post
|
||||||
|
Accept-Encoding: gzip, deflate, br
|
||||||
|
Accept-Language: en-GB,en;q=0.9,fi-FI;q=0.8,fi;q=0.7,en-CA;q=0.6,en-US;q=0.5,fr;q=0.4
|
||||||
|
```
|
||||||
|
|
||||||
|
##### El cuerpo del mensaje
|
||||||
|
|
||||||
|
Un mensaje HTTP puede tener un cuerpo de datos que se envía después de las líneas de cabecera. En una respuesta, es donde se devuelve al cliente el recurso solicitado (el uso más común del cuerpo del mensaje), o quizás un texto explicativo si hay un error. En una solicitud, es donde se envían al servidor los datos introducidos por el usuario o los archivos cargados.
|
||||||
|
|
||||||
|
Si un mensaje HTTP incluye un cuerpo, normalmente hay líneas de encabezado en el mensaje que describen el cuerpo. En particular,
|
||||||
|
|
||||||
|
La cabecera Content-Type: indica el tipo MIME de los datos del cuerpo (text/html, application/json, text/plain, text/css, image/gif). La cabecera Content-Length: indica el número de bytes del cuerpo.
|
||||||
|
Métodos de solicitud
|
||||||
|
|
||||||
|
GET, POST, PUT y DELETE son los métodos de petición HTTP con los que vamos a implementar una API o una aplicación de operaciones CRUD.
|
||||||
|
|
||||||
|
1. GET: El método GET se utiliza para recuperar y obtener información del servidor dado utilizando un URI dado. Las peticiones que utilizan GET sólo deben recuperar datos y no deben tener ningún otro efecto sobre los datos.
|
||||||
|
|
||||||
|
2. POST: La petición POST se utiliza para crear datos y enviarlos al servidor, por ejemplo, crear un nuevo post, subir archivos, etc. utilizando formularios HTML.
|
||||||
|
|
||||||
|
3. PUT: Sustituye todas las representaciones actuales del recurso de destino por el contenido subido y lo utilizamos modificar o actualizar datos.
|
||||||
|
|
||||||
|
4. DELETE: Elimina datos
|
||||||
|
|
||||||
|
|
||||||
[<< Day 27](../27_Python_con_MongoDB/README.md) | [Day 29 >>](../29_Construcción_de_API/README.md)
|
[<< Day 27](../27_Python_con_MongoDB/README.md) | [Day 29 >>](../29_Construcción_de_API/README.md)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Estos apuntes y ejercicios en python han sido realizados siguiendo un repositori
|
|||||||
|
|
||||||
El repositorio original está en inglés, pero he traducido los ejercicios al español, así como los apuntes que he creído convenientes resaltar.
|
El repositorio original está en inglés, pero he traducido los ejercicios al español, así como los apuntes que he creído convenientes resaltar.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Índice del curso y apuntes realizados
|
## Índice del curso y apuntes realizados
|
||||||
|
|
||||||
|
|||||||
62
README.md
62
README.md
@@ -2,18 +2,19 @@
|
|||||||
|
|
||||||
Este repositorio contiene los apuntes tomados en diversos cursos de python reflejados en la siguiente tabla:
|
Este repositorio contiene los apuntes tomados en diversos cursos de python reflejados en la siguiente tabla:
|
||||||
|
|
||||||
| Nombre del curso <br> y ubicación | Nivel | Duración <br> de vídeos | Duración personal <br> aprox. | Fuente original |
|
| Nombre del curso <br> y ubicación | Nivel | Duración <br> de vídeos | Duración <br> personal aprox. | Fuente original |
|
||||||
| ---------------------------------------------------------: | :--------: | :---------------------: | :---------------------------: | :------------------------------------------------------------------------------------------------------ |
|
| ---------------------------------------------------------: | :--------: | :---------------------: | :---------------------------: | :------------------------------------------------------------------------------------------------------ |
|
||||||
| [HolaMundo](./HolaMundo/README.md) | Bajo | 5 horas | 15 horas | [Aprende python ahora!](https://www.youtube.com/watch?v=tQZy0U8s9LY&ab_channel=HolaMundo) |
|
| [HolaMundo](./HolaMundo/README.md) | Bajo | 5 horas | 15 horas | [Aprende python ahora!](https://www.youtube.com/watch?v=tQZy0U8s9LY&ab_channel=HolaMundo) |
|
||||||
| [Python total](./python-total/README.md) | Intermedio | 30 horas | 200 horas | [Escuela Directa](https://www.udemy.com/course/python-total) |
|
| [Python total](./python-total/README.md) | Intermedio | 30 horas | 200 horas | [Escuela Directa](https://www.udemy.com/course/python-total) |
|
||||||
| [Python y ChatGPT](./python-chatgpt/README.md) | Intermedio | 2 horas | 15 horas | [Escuela Directa](https://www.udemy.com/course/python-chatgpt/), [ChapGPT](https://www.chat.openai.com) |
|
| [Python y ChatGPT](./python-chatgpt/README.md) | Intermedio | 2 horas | 15 horas | [Escuela Directa](https://www.udemy.com/course/python-chatgpt/), [ChapGPT](https://www.chat.openai.com) |
|
||||||
| [30 days of python](./30-days-of-python/README.md) | Intermedio | --- | próximamente | [Repo Asabeneh](https://github.com/Asabeneh/30-Days-Of-Python) |
|
| [30 days of python](./30-days-of-python/README.md) | Intermedio | --- | 25 horas | [Repo Asabeneh](https://github.com/Asabeneh/30-Days-Of-Python) |
|
||||||
| [scripts-hacking-etico](./scripts-hacking-etico/README.md) | Avanzado | --- | 5 horas | [ChapGPT](https://www.chat.openai.com), [Canal Telegram](https://t.me/seguridadinformatic4) |
|
|
||||||
| [Cajón de sastre](./catch-all/README.md) | Intermedio | --- | continua | [Personal](https://vergaracarmona.es) |
|
| [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**.
|
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**.
|
||||||
|
|
||||||
Si te parece útil este documento puedes agradecerlo a través de las vías de contacto de la [web](https://vergaracarmona.es) o [invítandome a un café ☕️ ⬇️](#invítame-a-un-café)
|
Si te parece útil este documento puedes agradecerlo a través de las vías de contacto de la [web](https://vergaracarmona.es) o [invítandome a un café ☕️](#invítame-a-un-café)
|
||||||
|
|
||||||
Recuerda,
|
Recuerda,
|
||||||
|
|
||||||
@@ -23,8 +24,10 @@ Recuerda,
|
|||||||
---
|
---
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Qué es python según chatGPT 🤖
|
## 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.
|
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.
|
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.
|
||||||
@@ -36,10 +39,11 @@ Una de las razones por las que Python ha ganado popularidad es su comunidad acti
|
|||||||
Además, Python es conocido por ser un lenguaje fácil de aprender y utilizar. Su sintaxis intuitiva y legible permite a los programadores escribir código de manera más rápida y eficiente, lo que reduce el tiempo de desarrollo y facilita la colaboración en proyectos.
|
Además, Python es conocido por ser un lenguaje fácil de aprender y utilizar. Su sintaxis intuitiva y legible permite a los programadores escribir código de manera más rápida y eficiente, lo que reduce el tiempo de desarrollo y facilita la colaboración en proyectos.
|
||||||
|
|
||||||
En resumen, Python es un lenguaje de programación de alto nivel, interpretado y fácil de aprender que se utiliza ampliamente en una variedad de aplicaciones, desde desarrollo web hasta análisis de datos e inteligencia artificial. Su enfoque en la legibilidad del código y su comunidad activa lo convierten en una elección popular entre los programadores.
|
En resumen, Python es un lenguaje de programación de alto nivel, interpretado y fácil de aprender que se utiliza ampliamente en una variedad de aplicaciones, desde desarrollo web hasta análisis de datos e inteligencia artificial. Su enfoque en la legibilidad del código y su comunidad activa lo convierten en una elección popular entre los programadores.
|
||||||
|
</details>
|
||||||
|
|
||||||
## Historia de python 📜
|
<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).
|
El nombre del lenguaje proviene de la afición de su creador por los humoristas británicos [Monty Python](https://youtu.be/aQqhR26FOW8).
|
||||||
|
|
||||||
@@ -50,19 +54,47 @@ Guido van Rossum es el principal autor de Python, y su continuo rol central en d
|
|||||||
> Guido van Rossum
|
> 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.
|
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>
|
||||||
|
|
||||||
## Últimas versiones
|
<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:
|
||||||
|
|
||||||
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.
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
En la actualidad, Python se aplica en los campos de inteligencia artificial y machine learning.
|
En la actualidad, Python se aplica en los campos de inteligencia artificial y machine learning.
|
||||||
|
|
||||||
## Información en tablas de python 📊
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Tabla desplegable de <strong>Usos de Python y sus Bibliotecas/módulos</strong></summary>
|
<summary>Tabla de <strong>Usos de Python y sus Bibliotecas/módulos 📚</strong></summary>
|
||||||
|
|
||||||
| Uso principal | Bibliotecas/módulos utilizados |
|
| Uso principal | Bibliotecas/módulos utilizados |
|
||||||
| -----------------------------------------------------: | :---------------------------------------------------------------------------- |
|
| -----------------------------------------------------: | :---------------------------------------------------------------------------- |
|
||||||
@@ -206,7 +238,7 @@ En la actualidad, Python se aplica en los campos de inteligencia artificial y ma
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>Tabla desplegable de <strong>comparación con otros lenguajes de programación</strong></summary>
|
<summary>Tabla de <strong>comparación con otros lenguajes de programación 🤔</strong></summary>
|
||||||
|
|
||||||
| Característica | Python | Java | C++ | JavaScript |
|
| Característica | Python | Java | C++ | JavaScript |
|
||||||
| --------------------- | ------------------------------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------- |
|
| --------------------- | ------------------------------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------- |
|
||||||
@@ -237,9 +269,9 @@ Esta tabla solo proporciona una comparación general entre los lenguajes y que c
|
|||||||
|
|
||||||
# Agradecimientos 🎁
|
# 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 he realizado con ellos y por todo el contenido libre de sus webs y canales de RRSS.
|
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.
|
||||||
|
|
||||||
También a todos los compas que me han apoyado en este camino.
|
Y por último, a todos los compas que me han apoyado en este camino.
|
||||||
|
|
||||||
> [Solo no puedes, con amigos sí. 🤝](https://youtu.be/Ds7tje_Y0CM)
|
> [Solo no puedes, con amigos sí. 🤝](https://youtu.be/Ds7tje_Y0CM)
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user