Compare commits

...

6 Commits

Author SHA1 Message Date
Jose
6e4c05665e Merge branch 'dev' of git.miarma.net:/Gallardo7761/aii-monorepo into dev 2026-03-07 22:09:39 +01:00
Jose
a0297672b2 add: whoosh 2026-03-07 22:09:00 +01:00
Jose
bcab404758 Finished 2026-02-22 12:45:08 +01:00
AlejandroJose2001
aa62898e2b Merge branch 'dev' of git.miarma.net:Gallardo7761/aii-monorepo into dev 2026-02-22 09:54:45 +01:00
AlejandroJose2001
00f5f12f7c Add: initial implementation of BeautifulSoup exercise 4 with SSL, config, database, and UI setup 2026-02-22 09:53:58 +01:00
Jose
d011ff10a5 Add: basic structure 2026-02-20 17:30:15 +01:00
8 changed files with 365 additions and 1 deletions

Binary file not shown.

View File

@@ -0,0 +1,5 @@
def init_ssl():
import os, ssl
if (not os.environ.get('PYTHONHTTPSVERIFY', '') and
getattr(ssl, '_create_unverified_context', None)):
ssl._create_default_https_context = ssl._create_unverified_context

View File

@@ -0,0 +1,6 @@
from pathlib import Path
BASE_URL = "https://recetas.elperiodico.com"
RECIPES_URL = BASE_URL + "/Recetas-de-Aperitivos-tapas-listado_receta-1_1.html"
DATA_DIR = Path(__file__).parent.parent / "data"
DB_PATH = DATA_DIR / "recipes.bd"

View File

@@ -0,0 +1,141 @@
import sqlite3
from pathlib import Path
class DBAttr:
def __init__(self, name, type_, modifier=""):
self.name = name
self.type_ = type_
self.modifier = modifier
def sql(self):
parts = [self.name, self.type_]
if self.modifier:
parts.append(self.modifier)
return " ".join(parts)
class DBManager:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, path):
self.path = Path(path)
self.conn = sqlite3.connect(self.path)
self.conn.row_factory = sqlite3.Row
def create_table(self, table_name, attributes: list[DBAttr]):
columns_sql = ",\n ".join(attr.sql() for attr in attributes)
query = f"""
CREATE TABLE IF NOT EXISTS {table_name} (
{columns_sql}
);
"""
try:
with self.conn:
self.conn.execute(query)
except Exception as e:
print("Error creating table:", e)
def get_all(self, table_name):
try:
cursor = self.conn.execute(f"SELECT * FROM {table_name};")
return [dict(row) for row in cursor.fetchall()]
except Exception as e:
print("Error selecting:", e)
return []
def get_singleton(self, singleton_table):
try:
cursor = self.conn.execute(f"SELECT * FROM {singleton_table}")
return [row[0] for row in cursor.fetchall()]
except Exception as e:
print("Error selecting:", e)
return []
def get_by(self, table_name, column, value):
try:
query = f"SELECT * FROM {table_name} WHERE {column} = ?;"
cursor = self.conn.execute(query, (value,))
return [dict(row) for row in cursor.fetchall()]
except Exception as e:
print("Error selecting:", e)
return []
def insert(self, table_name, data: dict):
keys = ", ".join(data.keys())
placeholders = ", ".join("?" for _ in data)
values = tuple(data.values())
query = f"""
INSERT INTO {table_name} ({keys})
VALUES ({placeholders});
"""
try:
with self.conn:
self.conn.execute(query, values)
except Exception as e:
print("Error inserting:", e)
def update(self, table_name, data: dict, where_column, where_value):
set_clause = ", ".join(f"{key} = ?" for key in data.keys())
values = list(data.values())
values.append(where_value)
query = f"""
UPDATE {table_name}
SET {set_clause}
WHERE {where_column} = ?;
"""
try:
with self.conn:
self.conn.execute(query, tuple(values))
except Exception as e:
print("Error updating:", e)
def delete(self, table_name, where_column, where_value):
query = f"DELETE FROM {table_name} WHERE {where_column} = ?;"
try:
with self.conn:
self.conn.execute(query, (where_value,))
except Exception as e:
print("Error deleting:", e)
def clear(self, table_name):
query = f"DELETE FROM {table_name};"
try:
with self.conn:
self.conn.execute(query)
except Exception as e:
print("Error clearing table: ", e)
def exists(self, table_name, where_column, where_value):
query = f"SELECT 1 FROM {table_name} WHERE {where_column} = ? LIMIT 1;"
try:
cursor = self.conn.execute(query, (where_value,))
return cursor.fetchone() is not None
except Exception as e:
print("Error checking existence:", e)
return False
def count(self, table_name):
try:
cursor = self.conn.execute(f"SELECT COUNT(*) as total FROM {table_name};")
return cursor.fetchone()["total"]
except Exception as e:
print("Error counting:", e)
return 0
def close(self):
self.conn.close()

View File

@@ -0,0 +1,132 @@
from bs4 import BeautifulSoup
import re
from tkinter import Tk
from tkinter import messagebox
import urllib.request
from datetime import datetime
import locale
from db import DBManager, DBAttr
from ui import RecipesUI
from __ssl import init_ssl
from config import *
init_ssl()
locale.setlocale(locale.LC_TIME, "es_ES.UTF-8")
dbm = DBManager(DB_PATH)
def create_tables():
recipes_attr = [
DBAttr("title", "TEXT", "NOT NULL"),
DBAttr("difficulty", "TEXT", "DEFAULT NULL"),
DBAttr("units", "INTEGER", "DEFAULT NULL"),
DBAttr("duration", "INTEGER", "DEFAULT NULL"),
DBAttr("author", "TEXT", "NOT NULL"),
DBAttr("updated_at", "DATE", "NOT NULL")
]
dbm.create_table("recipes", recipes_attr)
def parse_duration(duration):
if not duration:
return None
duration = duration.strip().lower()
hours = 0
minutes = 0
h_match = re.search(r"(\d+)h", duration)
m_match = re.search(r"(\d+)m", duration)
if h_match:
hours = int(h_match.group(1))
if m_match:
minutes = int(m_match.group(1))
return hours * 60 + minutes
def parse_duration_inverse(minutes):
if minutes is None:
return None
m = minutes % 60
h = (minutes - m) // 60
return f"{h}h {m}m" if h != 0 else f"{m}m"
def persist_recipes():
f = urllib.request.urlopen(RECIPES_URL)
bs = BeautifulSoup(f, "lxml")
results = bs.find_all("div", attrs={"data-js-selector": "resultado"})
for div in results:
title_a = div.a
title = title_a.string.strip()
info_div = div.find("div", class_="info_snippet")
difficulty = info_div.find("span").get_text(strip=True) if info_div and info_div.find("span") else None
properties = div.find("div", class_="properties")
duration = properties.find("span", class_="duracion").string.strip() if properties and properties.find("span", class_="duracion") else None
units = properties.find("span", class_="unidades").string.strip() if properties and properties.find("span", class_="unidades") else None
details_link = title_a["href"]
f2 = urllib.request.urlopen(details_link)
bs2 = BeautifulSoup(f2, "lxml")
details = bs2.find("div", class_="autor").find("div", class_="nombre_autor")
author = details.find("a").string
date_str = details.find("span").string.replace("Actualizado: ", "")
updated_at = datetime.strptime(date_str, "%d %B %Y")
dbm.insert("recipes", {
"title": title,
"difficulty": difficulty,
"units": units,
"duration": parse_duration(duration),
"author": author,
"updated_at": updated_at
})
return dbm.count("recipes")
def main():
create_tables()
root = Tk()
ui = RecipesUI(root)
def handle_action(action):
match(action):
case "cargar":
resp = messagebox.askyesno(title="Cargar", message="Quieres cargar todos los datos de nuevo?")
if resp:
dbm.clear("recipes")
recipes_count = persist_recipes()
ui.info(f"Hay {recipes_count} recetas")
case "listar_recetas":
recipes = dbm.get_all("recipes")
for r in recipes:
r["units"] = str(r["units"]) + " personas" if r["units"] is not None else "Unknown personas"
r["duration"] = parse_duration_inverse(r["duration"])
ui.show_list(recipes, ["title", "difficulty", "units", "duration"])
case "buscar_autor":
def search_author(author):
recipes = [recipe for recipe in dbm.get_all("recipes") if author.lower() in recipe["author"].lower()]
for r in recipes:
r["units"] = str(r["units"]) + " personas" if r["units"] is not None else "Unknown personas"
r["duration"] = parse_duration_inverse(r["duration"])
ui.show_list(recipes, ["title", "difficulty", "units", "duration", "author"])
ui.ask_text("Buscar por autor: ", search_author)
case "buscar_fecha":
def search_date(date):
d = datetime.strptime(date, "%d/%m/%Y")
recipes = [recipe for recipe in dbm.get_all("recipes")
if d > datetime.strptime(recipe["updated_at"], "%Y-%m-%d %H:%M:%S")]
for r in recipes:
r["units"] = str(r["units"]) + " personas" if r["units"] is not None else "Unknown personas"
r["duration"] = parse_duration_inverse(r["duration"])
ui.show_list(recipes, ["title", "difficulty", "units", "duration", "updated_at"])
ui.ask_text("Buscar por fecha: ", search_date)
ui.callback = handle_action
root.mainloop()
dbm.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,79 @@
import tkinter as tk
from tkinter import ttk, messagebox
from tkinter.scrolledtext import ScrolledText
class RecipesUI():
def __init__(self, root, title = "AII"):
self.root = root
self.root.title(title)
self.root.geometry("900x600")
# Menu Principal
self.menu = tk.Menu(self.root)
self.root.config(menu=self.menu)
# Menu Datos
datos_menu = tk.Menu(self.menu, tearoff=0)
datos_menu.add_command(label="Cargar", command=lambda: self.callback("cargar"))
datos_menu.add_separator()
datos_menu.add_command(label="Salir", command=self.root.quit)
self.menu.add_cascade(label="Datos", menu=datos_menu)
# Menu Listar
listar_menu = tk.Menu(self.menu, tearoff=0)
listar_menu.add_command(label= "Recetas", command = lambda: self.callback("listar_recetas"))
self.menu.add_cascade(label="Listar", menu=listar_menu)
# Menu Buscar
buscar_menu = tk.Menu(self.menu, tearoff=0)
buscar_menu.add_command(label="Receta por autor", command=lambda: self.callback("buscar_autor"))
buscar_menu.add_command(label="Receta por fecha", command=lambda: self.callback("buscar_fecha"))
self.menu.add_cascade(label="Buscar", menu=buscar_menu)
# Callback externo desde el punto de entrada
self.callback = None
def show_list(self, items, fields, title="Listado"):
mw = tk.Toplevel(self.root)
mw.title(title)
listbox = tk.Listbox(mw, width=80, height=20)
listbox.pack(side="left", fill="both", expand=True)
scrollbar = tk.Scrollbar(mw)
scrollbar.pack(side="right", fill="y")
listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=listbox.yview)
for item in items:
row = " | ".join(str(item[field]) for field in fields)
listbox.insert("end", row)
def ask_text(self, label, callback):
mw = tk.Toplevel(self.root)
mw.title(label)
tk.Label(mw, text=label).pack(pady=5)
entry = ttk.Entry(mw)
entry.pack(pady=5)
ttk.Button(mw, text="Aceptar", command=
lambda: [callback(entry.get()), mw.destroy()]).pack(pady=10)
def ask_spinbox(self, label, options, callback):
mw = tk.Toplevel(self.root)
mw.title(label)
tk.Label(mw, text=label).pack(pady=5)
spinbox = ttk.Spinbox(mw, values=options, state="readonly", width=40)
spinbox.pack(pady=5)
ttk.Button(mw, text="Aceptar", command=
lambda: [callback(spinbox.get()), mw.destroy()]).pack(pady=10)
def ask_radiobutton(self, label, options, callback):
mw = tk.Toplevel(self.root)
mw.title(label)
tk.Label(mw, text=label).pack(pady=5)
sv = tk.StringVar(value=options[0])
for option in options:
tk.Radiobutton(mw, text=option, variable=sv, value=option).pack(anchor="w")
ttk.Button(mw, text="Aceptar", command=
lambda: [callback(sv.get()), mw.destroy()]).pack(pady=10)
def info(slef, message):
messagebox.showinfo("Información", message)

View File

@@ -1,3 +1,4 @@
lxml lxml
bs4 bs4
requests requests
whoosh

0
whoosh/main.py Normal file
View File