Compare commits

...

150 Commits

Author SHA1 Message Date
92e473fdfe Add script to README 2025-11-08 22:44:22 +01:00
6e97f2bea1 Add script secret_santa.py 2025-11-08 22:31:34 +01:00
fa1ee4dd64 feat: add local date in docker-compose files 2025-07-12 17:10:20 +02:00
b9884d6560 Comments in docker-compose 2025-05-22 22:57:42 +02:00
9245dbb9af Add compose in clima_bot 2025-05-22 21:07:19 +02:00
2c4f14a886 Update mareas_bot 2025-02-21 21:31:58 +01:00
1fa360a109 mareas_bot: Fix errors. Update dependencies. Add enhancements 2025-02-19 23:27:15 +01:00
a2badd511d Fusionar feature/tablamareasbot en main 2025-02-19 21:24:50 +01:00
68b9bf32a3 Add rubiks cube solver 2024-09-01 21:48:24 +02:00
21c7e1e6c6 Add enhanced urlf4ck3r 2024-09-01 19:18:39 +02:00
dc9e81f06e Update Ollama bot 2024-08-18 21:15:33 +02:00
dd48618093 Update Ollama bot 2024-08-18 20:18:59 +02:00
3e47d5a7ee Add Ollama bot 2024-08-18 20:16:34 +02:00
d59e31205b Update chatgpt bot 2024-08-18 19:43:44 +02:00
e2767e99af Update chatgpt bot 2024-08-17 21:11:58 +02:00
65468e4115 Add chatgpt bot 2024-08-17 21:08:24 +02:00
4f2264748e Update README 2024-08-15 19:43:39 +02:00
a73e8a0222 Update diagrams test README 2024-08-13 01:46:47 +02:00
84a2519f6c Add diagrams test 2024-08-13 01:43:53 +02:00
88a26dae34 Update .gitignore 2024-08-13 01:30:37 +02:00
b672828792 Add sonarqube test 2024-08-09 02:22:44 +02:00
40fad6bae8 Add prometheus_grafana test 2024-08-09 01:20:19 +02:00
17a7e04180 Update README infra 2024-08-08 00:03:17 +02:00
6cb79e3418 Add structure prometheus_grafana & sonarqube 2024-08-08 00:01:27 +02:00
28f9bb389d Add kafka test 2024-08-07 23:59:44 +02:00
f525beaf11 Update .gitignore 2024-08-07 23:59:06 +02:00
4756d756ac Update elastic stack test 2024-08-07 19:56:05 +02:00
89959d29ee Update rabbitmq test 2024-08-06 22:05:20 +02:00
898da84dc3 Update rabbitmq test 2024-08-05 23:37:04 +02:00
bba89794a2 Update .gitignore 2024-08-05 23:36:38 +02:00
78552c227a Update rabbitmq test 2024-08-04 20:56:58 +02:00
9a60b44822 Update .gitignore 2024-08-04 20:55:49 +02:00
e856e99ac3 Update READMEs 2024-08-02 20:10:03 +02:00
7102b86e6e Update README catch-all 2024-08-02 20:07:49 +02:00
50513ff393 Add rabbitmq test 2024-07-31 23:27:53 +02:00
0daba91bbb Update README infra test 2024-07-31 13:38:57 +02:00
a7cefe06d0 Add rabbitmq test 2024-07-30 23:55:17 +02:00
ecd77967a0 Add movies trivial Bot for Telegram 2024-07-30 00:43:20 +02:00
be39d5b1d3 Re-structure infra folder 2024-07-22 21:09:54 +02:00
d7640c2a52 Update movie_bot 2024-07-22 20:55:34 +02:00
2b946f3327 Update README Bots Telegram 2024-07-17 21:17:07 +02:00
5ff0fa4f2a Update movie Bot for Telegram 2024-07-17 18:44:16 +02:00
a421d3b292 Add movie Bot for Telegram 2024-07-17 01:10:27 +02:00
84e3344d49 Add rss Bot for Telegram 2024-07-16 00:48:20 +02:00
56ac284176 Update .gitignore 2024-07-15 23:59:31 +02:00
94e6ca1027 Update README 2024-07-15 23:41:28 +02:00
819aaaa1f5 Add Weather Bot for Telegram 2024-07-14 19:20:40 +02:00
d8ca020c98 Update README 2024-07-13 14:20:03 +02:00
fbecbbf0a1 Add Translator Bot for Telegram 2024-07-11 02:54:47 +02:00
5821636a01 Update Bots Telegram 2024-07-10 01:06:53 +02:00
62d0ec78b5 Add bots telegram 2024-07-10 01:03:15 +02:00
71d0a3c4d8 Add new exercise 2024-06-16 18:00:16 +02:00
d7c1cee2a8 Add link shortener in README 2024-02-27 23:29:08 +01:00
c1c5883a56 Add link shortener 2024-02-27 23:20:35 +01:00
485d16e930 Fix errors 2024-02-20 19:12:25 +01:00
35727e1664 Update README 2024-02-19 23:52:29 +01:00
a55a297cdb Add weather script 2024-02-19 23:47:25 +01:00
69389a8940 Update versions 2024-02-13 20:26:34 +01:00
3d5486414f Update README 2024-02-04 01:16:11 +01:00
d7252c6782 Update forwardshell script 2024-02-03 12:28:27 +01:00
d4c5d8a50f wip 2024-02-03 12:27:47 +01:00
5d7e3302c5 wip 2024-02-02 23:02:37 +01:00
0d95645a93 Delete backdoor.exe 2024-02-01 19:19:01 +01:00
3704f6c61e Update backdoor & listener - C2 2024-02-01 19:18:06 +01:00
d505f6ee23 Remove malware.exe 2024-02-01 19:17:06 +01:00
bcb3977c07 wip 2024-02-01 01:27:14 +01:00
6ae9c26e65 Add backdoor 2024-02-01 01:26:44 +01:00
d1a05d481b Update malware 2024-02-01 01:26:21 +01:00
21313f990b Update keylogger 2024-02-01 01:26:01 +01:00
213a859a7d Add malware 2024-01-31 20:29:22 +01:00
352889e2ba Add keylogger 2024-01-30 22:49:34 +01:00
2de0131f95 Add traffic hijacking 2024-01-30 18:42:58 +01:00
f7043ed2ae Add dns spoofer 2024-01-30 18:42:35 +01:00
d828243927 Add intercept packets script 2024-01-30 18:42:12 +01:00
04be4f610d Add images sniffer 2024-01-29 19:29:24 +01:00
ace46c757a Add https sniffers with mitmdump 2024-01-29 19:29:03 +01:00
4abbfdf261 Add sniffer scapy 2024-01-29 19:28:10 +01:00
92ed9c3beb Update arp spoofer & dns sniffer 2024-01-29 19:27:46 +01:00
c86c4f8b21 Update .gitignore 2024-01-29 19:27:00 +01:00
4847e32f04 Add DNS sniffer 2024-01-28 23:41:08 +01:00
371cd18bf0 Add arp spoof 2024-01-28 22:35:39 +01:00
cb7b2c2e13 Update network scan 2024-01-28 20:53:14 +01:00
4890661532 Update network scan & add arp scan 2024-01-28 19:38:36 +01:00
222649cd8d Add network scan 2024-01-28 16:06:28 +01:00
8f2913d463 Update README 2024-01-26 19:42:05 +01:00
7f601e84a7 Add mac changer 2024-01-26 19:40:27 +01:00
7dddcfaa28 Add port scan 2024-01-26 18:13:36 +01:00
386c544294 Update READMEç 2024-01-25 23:16:13 +01:00
9801b42111 Update README 2024-01-25 23:13:01 +01:00
5be31fd800 Update README 2024-01-25 23:11:44 +01:00
80e7e4a21c Update .gitignore 2024-01-25 23:09:25 +01:00
b8187724d2 Update chat project 2024-01-25 23:06:43 +01:00
c0b6b881e4 Add chat project 2024-01-25 22:26:39 +01:00
bd197dc519 Add calculator project 2024-01-24 18:45:39 +01:00
81e279e30a Add notepad project 2024-01-24 18:45:08 +01:00
23805c8241 Add library exercises 2024-01-24 18:44:22 +01:00
cf745e8c8d Add library exercises 2024-01-23 21:21:18 +01:00
700c489428 Add library exercises 2024-01-19 21:34:36 +01:00
658f94c62b Add library exercises 2024-01-17 20:50:25 +01:00
d15f5b4eb6 Add libraries exercises 2024-01-17 19:25:30 +01:00
062908b4ee Update README 2024-01-16 22:33:36 +01:00
72ccded05d Update requests exercise 2024-01-14 00:15:52 +01:00
17d758d75b Update descifrador 2024-01-13 20:37:11 +01:00
9ad2161b99 Add librerias exercices 2024-01-13 20:31:41 +01:00
36973a9662 Add wargame decode 2024-01-13 16:58:21 +01:00
8109c2fe30 Update Python Ofensivo - Más ejercicios con conexiones socket 2024-01-11 23:08:41 +01:00
f09fa0cbf5 Update Python Ofensivo - Pruebas de conexión UDP con la librería socket 2024-01-11 21:43:05 +01:00
ec29b5cde0 Update Python Ofensivo - Pruebas de conexión con la librería socket 2024-01-11 20:26:58 +01:00
653e96cc2b Update Python Ofensivo - Proyecto gestor de notas 2024-01-11 20:26:18 +01:00
3ce3338795 Update Python Ofensivo - validador de correo con regex 2024-01-11 20:25:12 +01:00
818ec91771 Update Python Ofensivo 2024-01-10 23:29:44 +01:00
bb39d2366f Update Python Ofensivo 2024-01-10 22:55:31 +01:00
1f8a74e791 Update Python Ofensivo 2024-01-09 23:34:35 +01:00
661b027ccc Update Python Ofensivo 2024-01-09 22:57:42 +01:00
7c2ccb390c Update Python Ofensivo 2024-01-08 22:36:16 +01:00
d49859c627 Update Python Ofensivo 2023-12-29 07:05:48 +01:00
159d9490a4 Update Python Ofensivo 2023-12-29 05:35:02 +01:00
4c142e7432 Update Python Ofensivo 2023-12-28 08:25:43 +01:00
e300dee83f Update Python Ofensivo 2023-12-26 23:56:20 +01:00
490afce41f Update Python Ofensivo 2023-12-25 21:16:48 +01:00
ab95e6f805 Update Python Ofensivo 2023-12-25 10:54:18 +01:00
8896ac39c2 Update Python Ofensivo 2023-12-24 09:06:20 +01:00
3402445513 Update Python Ofensivo 2023-12-24 08:54:53 +01:00
54f7024e20 Update Python Ofensivo 2023-12-21 20:06:55 +01:00
51ed9bc844 Update Python Ofensivo 2023-12-20 20:38:59 +01:00
9d7531b0f1 Update Python Ofensivo 2023-12-20 18:51:37 +01:00
be1177090b Update Python Ofensivo 2023-12-19 23:59:04 +01:00
a307064326 Update Python Ofensivo 2023-12-17 16:00:46 +01:00
85549ac613 Update Python Ofensivo 2023-12-14 21:55:17 +01:00
086b1ca614 Add course Python ofensivo
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-12-13 23:07:30 +01:00
5b62da6cf0 Update README
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-12-13 17:54:23 +01:00
Manuel Vergara
af9e0b66d9 Merge pull request #1 from manuelver/dependabot/pip/30-days-of-python/26_Desarrollo_web_en_Python/web/werkzeug-3.0.1
Bump werkzeug from 3.0.0 to 3.0.1 in /30-days-of-python/26_Desarrollo_web_en_Python/web
2023-10-31 23:03:55 +01:00
dependabot[bot]
219078c375 Bump werkzeug in /30-days-of-python/26_Desarrollo_web_en_Python/web
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.0...3.0.1)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 21:53:54 +00:00
db00df2e56 Update README
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-11 00:12:44 +02:00
a320317a9e Add exercises 28
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-10 23:07:29 +02:00
61d38a9b7b Update exercises 27
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-10 00:09:52 +02:00
a0d26af9ed Add exercises 27
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-10 00:05:16 +02:00
6fb5471334 Update README
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-09 20:49:10 +02:00
08d20fee61 Add exercises 26
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-09 00:00:03 +02:00
d8426a91d9 Add exercises 25
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-08 22:30:44 +02:00
398494e2f5 Add exercises 24 part 2
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-08 22:00:31 +02:00
d6a26ea924 Add exercises 24 part 1
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-08 21:51:12 +02:00
40d20c3bcf Update README
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-06 22:30:44 +02:00
8afad81939 Update README
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-06 22:19:28 +02:00
2ff37fdb2b Update README
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-06 22:17:06 +02:00
a8c1199c90 Add exercises 23
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-05 02:23:51 +02:00
94c89522d1 Add exercises 21 - Solved 3
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-05 02:04:06 +02:00
7b2cd6c376 Add exercises 21 - Solved 2
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-05 00:01:26 +02:00
6677000878 Add exercises 21 - Solved 1
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-04 23:17:38 +02:00
2cd9b703b2 Add exercises 21 - Not solved
Signed-off-by: Manuel Vergara <manuel@vergaracarmona.es>
2023-10-03 23:50:51 +02:00
338 changed files with 43505 additions and 20 deletions

33
.gitignore vendored
View File

@@ -127,9 +127,11 @@ celerybeat.pid
# Environments
.env
*.env
.venv
env/
venv/
myenv/
ENV/
env.bak/
venv.bak/
@@ -164,3 +166,34 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# certificate files
# Ignore all certificates in this directory
*.pem
*.key
*.csr
# Ignore binary files of mitmproxy
mitmproxy/
# Logs
bot.log*
logs/
*.log
# Ignore database files
rss.db
# Ignore vscode settings
.vscode
# Ignore volume files
rabbitmq/
rabbitmq/*
*_data
*_data/*

View 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")

View 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")

View 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")

View File

@@ -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/').
[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.
[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.
[Solución](03_web_scraping.py)
[<< Day 21](../21_Clases_y_objetos/README.md) | [Day 23 >>](../23_Entorno_virtual/README.md)

View 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"
}
}

View 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."
}
}

View 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"
}
]

View File

@@ -0,0 +1 @@
venv

View File

@@ -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.
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
```
![](pic/venv.png)
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View 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()

View 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()

View File

@@ -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)
[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)

View 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()

View File

@@ -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".
- 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)

File diff suppressed because it is too large Load Diff

View File

@@ -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.
[Solución](./web/)
[<< Day 25](../25_Pandas/README.md) | [Day 27 >>](../27_Python_con_MongoDB/README.md)

View File

@@ -0,0 +1 @@
web: python app.py

View 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)

View File

@@ -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

View File

@@ -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%;
}
}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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)

View 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)

View File

@@ -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)

View 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)

View File

@@ -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)
| 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)

View File

@@ -4,6 +4,115 @@ Documento original en inglés: [API](https://github.com/Asabeneh/30-Days-Of-Pyth
## 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.
![](https://github.com/Asabeneh/30-Days-Of-Python/raw/master/images/http_request_response_cycle.png)
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.
![](https://github.com/Asabeneh/30-Days-Of-Python/raw/master/images/request_response_header.png)
#### 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)

View File

@@ -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.
![](https://media.tenor.com/LcLprEbYSXAAAAAC/willy-fog.gif)
![](https://c.tenor.com/Zdpc10JrZrIAAAAC/tenor.gif)
## Índice del curso y apuntes realizados

View File

@@ -2,18 +2,19 @@
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) |
| [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) |
| [30 days of python](./30-days-of-python/README.md) | Intermedio | --- | próximamente | [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) |
| [30 days of python](./30-days-of-python/README.md) | Intermedio | --- | 25 horas | [Repo Asabeneh](https://github.com/Asabeneh/30-Days-Of-Python) |
| [Cajón de sastre](./catch-all/README.md) | Intermedio | --- | continua | [Personal](https://vergaracarmona.es) |
| [scripts-hacking-etico](./scripts-hacking-etico/README.md) | Avanzado | --- | 5 horas | [ChapGPT](https://www.chat.openai.com), [Canal Telegram](https://t.me/seguridadinformatic4) |
| [Python ofensivo](./python-ofensivo/README.md) | Avanzado | 35 horas | 100 horas | [hack4u](https://hack4u.io) |
Las prácticas y ejercicios aquí contenidos son los que hice mediante los cursos mencionados o por investigación propia, con mucho café para combatir el insomnio. Realmente, los apuntes no fueron pensados para compartirlos, por ello pueden tener lagunas de información o contenido adicional respecto al curso, ya que se redactaron para recordar procedimientos y conceptos que EMHO me parecieron relevantes. Teniendo estos documentos tan completos y entendiendo que el conocimiento debe ser libre, se decidió compartirlos. Si encuentras **cualquier error puedes abrir una issue o contactar conmigo**.
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,
@@ -23,8 +24,10 @@ Recuerda,
---
<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.
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.
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 📜
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.
<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.
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
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.
En la actualidad, Python se aplica en los campos de inteligencia artificial y machine learning.
## Información en tablas de python 📊
</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 |
| -----------------------------------------------------: | :---------------------------------------------------------------------------- |
@@ -206,7 +238,7 @@ En la actualidad, Python se aplica en los campos de inteligencia artificial y ma
</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 |
| --------------------- | ------------------------------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------- |
@@ -237,9 +269,9 @@ Esta tabla solo proporciona una comparación general entre los lenguajes y que c
# Agradecimientos 🎁
Por supuesto, quiero agradecer a [Federico Garay](https://ar.linkedin.com/in/fedegaray) y a [Nicolás Schürmann](https://www.linkedin.com/in/nicolasschurmann/) (¡Cuidao con el [teclado](https://youtu.be/y0T8UqBkawQ) que se gasta!) por los cursos en concreto que 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)

View 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()

View 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()

View 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()

View 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)

View 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"
}
]

View 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/

View 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"]

View 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 -->
![](https://res.cloudinary.com/practicaldev/image/fetch/s--MflWnlWv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4350/1%2AgEpkD_3NMTxK-w96c_QBBA.png)
## Prueba 1: Sin Redis
Primero vamos a hacer una prueba de la aplicación sin redis.
Vamos al directorio donde queremos trabajar, creamos un entorno virtual y lo activamos:
```bash
python3 -m venv venv
source venv/bin/activate
```
Ahora instalamos las dependencias en nuestro entorno:
```bash
(venv) pip install Flask redis flask_caching requests
```
Y guardamos estas dependencias en un archivo `requirements.txt`:
```bash
(venv) pip freeze > requirements.txt
```
Vamos a crear un archivo `app.py` con el siguiente contenido:
```python
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/universities")
def get_universities():
API_URL = "http://universities.hipolabs.com/search?country="
search = request.args.get('country')
r = requests.get(f"{API_URL}{search}")
return jsonify(r.json())
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)
```
Ahora vamos a ejecutar la aplicación:
```bash
export FLASK_APP=app.py
export FLASK_ENV=development
flask run
```
Y si nos vamos a postman podremos comprobar cuanto tarda en responder la petición:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--cFeIXNRd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2898/1%2A9o8_94EDMwGO-BWWKnshBA.png)
## Prueba 2: Dockerizar nuestra aplicación
Vamos a dockerizar nuestra aplicación, para ello vamos a crear un archivo `Dockerfile` con el siguiente contenido:
```Dockerfile
FROM python:3.12-alpine AS builder
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]
```
Vamos a crear también un archivo `docker-compose.yaml` con el siguiente contenido:
```yaml
version: '3.8'
services:
api:
container_name: app-python-flask-with-redis
build: .
env_file:
- .env
ports:
- '5000:5000'
depends_on:
- redis
redis:
image: redis:7.0-alpine
container_name: redis-python
ports:
- '6379:6379'
```
Incluimos también el contenedor de Redis. Lanzamos nuestra aplicación con el comando:
```bash
docker-compose up -d --build
```
Si vemos `docker ps` veremos que tenemos dos contenedores corriendo. También podemos revisar los logs del contenedor de la aplicación con `docker logs app-python-flask-with-redis`.
Comprobamos que nuestra aplicación sigue funcionando en docker igual que lo hacía en local.
## Prueba 3: Añadir Redis a nuestra aplicación
Ahora vamos a añadir Redis a nuestra aplicación. Vamos a modificar el archivo `app.py` para que use Redis:
```python
import requests
from flask import Flask, jsonify, request
from flask_caching import Cache
app = Flask(__name__)
app.config.from_object('config.BaseConfig')
cache = Cache(app)
@app.route("/universities")
@cache.cached(timeout=30, query_string=True)
def get_universities():
API_URL = "http://universities.hipolabs.com/search?country="
search = request.args.get('country')
r = requests.get(f"{API_URL}{search}")
return jsonify(r.json())
if __name__ == '__main__':
app.run(host='0.0.0.0')
```
Y vamos a crear un archivo `config.py` con el siguiente contenido:
```python
import os
class BaseConfig(object):
CACHE_TYPE = os.environ['CACHE_TYPE']
CACHE_REDIS_HOST = os.environ['CACHE_REDIS_HOST']
CACHE_REDIS_PORT = os.environ['CACHE_REDIS_PORT']
CACHE_REDIS_DB = os.environ['CACHE_REDIS_DB']
CACHE_REDIS_URL = os.environ['CACHE_REDIS_URL']
CACHE_DEFAULT_TIMEOUT = os.environ['CACHE_DEFAULT_TIMEOUT']
```
Este fichero recoge las variables de entorno que vamos a usar en nuestra aplicación. Vamos a crear un archivo `.env` con el siguiente contenido:
```bash
# .e
CACHE_TYPE=redis
CACHE_REDIS_HOST=redis
CACHE_REDIS_PORT=6379
CACHE_REDIS_DB=0
CACHE_REDIS_URL=redis://redis:6379/0
CACHE_DEFAULT_TIMEOUT=300
```
Al finalizar la práctica, tendremos esta estructura:
```
.
├── app.py
├── config.py
├── docker-compose.yaml
├── Dockerfile
├── .env
└── requirements.txt
```
Volvemos a lanzar nuestra aplicación con `docker-compose up -d --build` y comprobamos que todo sigue funcionando correctamente.
Volvemos a lanzar la misma petición desde postman y comprobamos que la respuesta es mucho más rápida que antes:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--mHDsWyzq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2922/1%2Au4a8OUBQu2gBzvc6iL5nsw.png)
Podemos probar con otros países, la primera vez tardará más porque no estará en caché, pero las siguientes veces será mucho más rápido.
Esta es la magia de Redis, una base de datos en memoria que nos permite almacenar datos en caché y acelerar nuestras aplicaciones 🚀

View 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')

View 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']

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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()

View File

@@ -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()

View 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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View 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()

View File

@@ -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()

View 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()

View 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()

View 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.
![](https://pica.zhimg.com/v2-35910cd84c7a62ad06cd4621b3d0523b_720w.jpg)
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).
![](https://mail.bogotobogo.com/python/images/RabbitMQ_Celery/WorkQueues/WorkQueues.png)
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.
![](https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.myanglog.com%2Fstatic%2Fe63b9118f1113a569006637046857099%2F7842b%2FUntitled1.png)
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.
![](https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Froytuts.com%2Fwp-content%2Fuploads%2F2022%2F02%2Fimage-2.png&f=1&nofb=1&ipt=7fde4343c19d799a6854664651f05dba983d70b2da4179f5b1f9d1e045a941d9&ipo=images)
#### 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.
![](https://miro.medium.com/v2/resize:fit:1400/0*gFwb04MsfqtVB5bY.png)
#### ¿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.
![](https://alvaro-videla.com/images/RPC-OverRMQ.png)
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.
---

View 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

View 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.__
![Kafka Diagram](https://miro.medium.com/v2/resize:fit:720/format:webp/1*kQXkMQTrMrG4VJ3KZehaqA.png)
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
![Kafka Concepts](https://miro.medium.com/v2/resize:fit:640/format:webp/1*48ck-bvatKzEpVapVa4Mag.png)
### 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.
![](https://miro.medium.com/v2/resize:fit:720/format:webp/1*Pp5vDC3T6OVWMHeWLdiIqA.png)
## 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í:
![](https://miro.medium.com/v2/resize:fit:720/format:webp/1*lESE2igQKfpd-QmhG39p0w.png)
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.
![](https://miro.medium.com/v2/resize:fit:720/format:webp/1*p3omv-7mRFzPA_ruCAJ6Mg.png)
## 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.
![](https://miro.medium.com/v2/resize:fit:720/format:webp/1*GmHD3GdjHV8ad2AzCIA_WA.png)
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.
---

View File

@@ -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"]

View File

@@ -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()

View File

@@ -0,0 +1 @@
kafka-python

View 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

View File

@@ -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"]

View File

@@ -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()

View File

@@ -0,0 +1,3 @@
kafka-python
requests
beautifulsoup4

View File

@@ -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"]

View File

@@ -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()

View File

@@ -0,0 +1,4 @@
kafka-python
requests
beautifulsoup4
lxml

View 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.

View 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"]

View 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()

View File

@@ -0,0 +1,3 @@
pandas==2.0.1
numpy==1.24.2
elasticsearch==8.8.0

View File

@@ -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
1 Title Origin/Ethnicity Director Cast Genre Plot Release Year Wiki Page
2 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
3 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
4 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
5 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
6 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
7 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

View 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

View File

@@ -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 }
}

View 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"]

View 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.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--dckjv7Wt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.thepylot.dev/content/images/size/w1000/2022/06/Screen-Shot-2022-06-11-at-16.51.10.png)
3. A continuación, selecciona Prometheus como fuente de datos:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--v5aNeycU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.thepylot.dev/content/images/size/w1000/2022/06/Screen-Shot-2022-06-11-at-16.53.47.png)
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`.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--cSw8NHIy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.thepylot.dev/content/images/size/w1000/2022/06/Screen-Shot-2022-06-11-at-16.56.09.png)
5. Finalmente, haz clic en el botón **Save & Test** para verificar la conexión de la fuente de datos.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--i4CVmg76--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%
2Cw_800/https://www.thepylot.dev/content/images/size/w1000/2022/06/Screen-Shot-2022-06-11-at-16.56.55.png)
¡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.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--yZiKAdWz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.thepylot.dev/content/images/size/w1000/2022/06/Screen-Shot-2022-06-11-at-17.02.21.png)
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.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--E8k1_d3s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.thepylot.dev/content/images/size/w1000/2022/06/Screen-Shot-2022-06-11-at-17.14.32.png)
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
```
---

View 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()

View File

@@ -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

View 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

View File

@@ -0,0 +1 @@
prometheus-client

View 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!

View File

@@ -0,0 +1,6 @@
def greet(name):
return f"Hola, {name}!"
if __name__ == "__main__":
print(greet("Mundo"))

View File

@@ -0,0 +1,6 @@
def add(a, b):
return a + b
def subtract(a, b):
return a - b

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

View File

@@ -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

View File

@@ -0,0 +1,16 @@
# Pruebas de despliegues en local de infraestructuras
<div style="display:block; margin-left:auto; margin-right:auto; width:50%;">
![](https://www.lineadatascan.com/wp-content/uploads/2018/10/Infraestructura.gif)
</div>
| Nombre | Descripción | Nivel |
| ------------------------------------------------------- | ----------------------------------------------------- | ---------- |
| [redis](./01_redis_flask_docker/) | Despliegue de redis y flask | Básico |
| [rabbit](./02_rabbitmq/README.md) | Despliegue de distintas arquitecturas para rabbitmq | Intermedio |
| [Apache Kafka](./03_kafka/README.md) | Despliegue de Apache Kafka con productor y consumidor | Intermedio |
| [Elastic stack](./04_elastic_stack/README.md) | Despliegue de Elastic Stack | Básico |
| [Prometheus Grafana](./05_prometheus_grafana/README.md) | Despliegue de Prometheus y Grafana para medir Python | Básico |
| [SonarQube](./06_sonarqube/README.md) | Despliegue de SonarQube para analisis de Python | Básico |

View File

@@ -0,0 +1,16 @@
import telebot
import os
from dotenv import load_dotenv
load_dotenv('.env')
BOT_TOKEN = os.getenv('BOT_TOKEN')
bot = telebot.TeleBot(BOT_TOKEN)
@bot.message_handler(commands=['get_group_id'])
def get_group_id(message):
chat_id = message.chat.id
bot.reply_to(message, f"El ID de este grupo es: {chat_id}")
bot.polling()

View File

@@ -0,0 +1,43 @@
"""
Pruebas con un bot de Telegram
Doc: https://github.com/eternnoir/pyTelegramBotAPI
"""
# bot.py
# Este es el script principal que ejecuta el bot de Telegram.
import signal
from termcolor import colored
from handlers import bot
from logger import logger
def def_handler(sig, frame):
"""
Manejador de señal para cerrar el programa de forma segura.
Args:
sig: Señal recibida.
frame: Frame actual.
"""
print(colored(
f"\n\n[!] Saliendo del programa...\n", "red", attrs=["bold"]
))
logger.info("Bot detenido por el usuario.")
bot.stop_polling()
exit(1)
# Configurar el manejador de señal para SIGINT (Ctrl+C)
signal.signal(signal.SIGINT, def_handler)
# Iniciar el bot
if __name__ == "__main__":
try:
logger.info("Bot iniciado.")
bot.infinity_polling()
except Exception as e:
logger.error(f"Error en la ejecución del bot: {str(e)}")
except KeyboardInterrupt:
def_handler(None, None)

View File

@@ -0,0 +1,19 @@
# config.py
# Este módulo gestiona la configuración y la carga de variables de entorno.
import os
from dotenv import load_dotenv
# Cargar las variables de entorno desde el archivo .env
load_dotenv('.env')
# Obtener el token del bot y el ID del chat de grupo desde las variables de entorno
BOT_TOKEN = os.getenv('BOT_TOKEN')
GROUP_CHAT_ID = os.getenv('GROUP_CHAT_ID')
# Validar que las variables de entorno estén configuradas
if not BOT_TOKEN or not GROUP_CHAT_ID:
raise AssertionError("Por favor, configura las variables de entorno BOT_TOKEN y GROUP_CHAT_ID")
# Convertir GROUP_CHAT_ID a entero
GROUP_CHAT_ID = int(GROUP_CHAT_ID)

Some files were not shown because too many files have changed in this diff Show More