From bcab40475855f621d58ef05d9814ae793c8694b7 Mon Sep 17 00:00:00 2001 From: Jose Date: Sun, 22 Feb 2026 12:45:08 +0100 Subject: [PATCH] Finished --- ejercicios/beautifulsoup/ej4/data/recipes.bd | Bin 8192 -> 16384 bytes .../beautifulsoup/ej4/data/src/__ssl.py | 5 - .../beautifulsoup/ej4/data/src/config.py | 6 - ejercicios/beautifulsoup/ej4/data/src/db.py | 141 ------------------ ejercicios/beautifulsoup/ej4/data/src/main.py | 85 ----------- ejercicios/beautifulsoup/ej4/src/main.py | 94 +++++++++--- .../beautifulsoup/ej4/{data => }/src/ui.py | 4 +- 7 files changed, 75 insertions(+), 260 deletions(-) delete mode 100644 ejercicios/beautifulsoup/ej4/data/src/__ssl.py delete mode 100644 ejercicios/beautifulsoup/ej4/data/src/config.py delete mode 100644 ejercicios/beautifulsoup/ej4/data/src/db.py delete mode 100644 ejercicios/beautifulsoup/ej4/data/src/main.py rename ejercicios/beautifulsoup/ej4/{data => }/src/ui.py (97%) diff --git a/ejercicios/beautifulsoup/ej4/data/recipes.bd b/ejercicios/beautifulsoup/ej4/data/recipes.bd index 861aaa4a38c9cf3e7829138b34e20f435142d52e..dbe7619a2ddf231c570eaa237a6bd4e0597afdb3 100644 GIT binary patch literal 16384 zcmeHNJ8T@s8Q$eXLs5Lli65~QF=HpPEt4zXgAzppf^(!!l&qt~#~wq{FsQlR(Q%c% z-Ss{QQ-}*8KmZphqcka^GKF9mLaJ2B0X7U*E?lK?<32@-AZhZ?9B+5_ag1EVZstUS zqK^9J-|zqb?|%$8*Be$y@m<&NkPugu(~7DpKgU>6lzI3ZfzSR+g}-FK@4q+b>%4N| zxBs4mch%`p1wQ0UG9Vd{3`hnf1CjyBfMh^2AQ_MhNCqSW-yZ`HK2XsMXU?d?5K|_K~ir1)Tg)Tmz4!!T%#Br}$y;Te`af{p|THU6u1ACKeR$%N?zf!8q zYvqboxrj@pxA@2U8)&(zsAmOKop{6q+yEQhCq_tZ0ksny_K>eL#E3dE*9Zg(t~-Vm9GP2zh=`eJ{0zORDXJ#GHoA|Twi^gQ9co%+SKp_2 zi`oY?#5xH=+Ix6FTa3|~Kjo1iqUppBiOu5IAf8VHD&TShS1p53FfknpDcG%7P#vCK zgk7cw7DPC6m`^PS@H4pD7IA0*1T9g&6PfgW&s+$DBp2R&8QmU?YwVMbXLUbu0q1GK zD+@6f+Zf$|KoA|al(>l*{(eLQF~Z_7$os6SRW8TK&7YuJ#%I@~4uma)*S=*_hluv} zoHvM#YB`2|ZwB22gGH9tz|UNGAY{9b!Gl3Mm1B#GV`E;-=R0RnQ&WaS_*6c77G&KZ z`;~{q4*(}5(YNzso+9E2o5Ti44+IV}&vrX43j`$V%wpp~Gm=QTa|Z3rswrnC*+)fL zG6)D$_lc7^WJ1e3^`cfzBx(I6RLh*O;`$)VeYFd+c8~8CLMBExU(97AHyT1OWLb$9 zFD+)w952qsi&`ZS<~u(^?`Fia??~JxwoThSWFVzUD}v0<9I;!dp_fFhTYUjlA;87T zr&Gw5L-#P1FHDxSn!_a7#x;W;Q1eLRE@|Z?UpAgcS2A!lpR$l3Evu7>=;JV}r8rsW zb1a(GML{?FlEft5rbprwT5VsHJf4j+%QjQ(8&3C60p4&;zx%iy^%d%}7!^CuqMI4h z*GNd<2pR}&KrY{lSy>QS(SMk$+BMT{vHl_eafND?E2%Suu3Hd;k#pWbh0l$eR>#h%XfIazfpoOvXWiTf|<&suOG;MbRH8Y?~uHDCEs@!XcvPSb( zwh%ihuN$Y(70^pjBY09nKj@fNx&$)3acDX7LQ#g6Ga+GjocC@b2?DzvXgFNLA~}_A zM-B2!-rllSPVC(|$*b!CY+%5L9_Cv>SCYcLJ(lY?k%i@grp*r?p0mkIlstv+^yMT57o$jLm zd(;nH2L?81K^l=2VBHOf{}zT`q2ENsEd-I#X*E&76bh;D9kZrl{YV27JSF2 z5I7z@mZTb+|Iy^OGX4AM3sZla+Mjx6@-OhMd`Si*1CjyBfMh^2AQ_MhNCqSWk^#xU zQ!+p*eDNzT%V0<3^Gg`Gn1g{0K9X@;TsZKJPQh*&ZYN=KO!ixPIk%KtJGx3FZQn8@ z+YlFh95#QI*h5V&xtgWig^3PpdKf6#L>j@OGq)KiS|0XblR0zyO@7}X4(@zS%{C1K zmb1*@-NH*+pdncx-n)Qk<|>BYqp`X99_g?}z+vzD2)@GgD*J`{)Fbq7Z3j}G+?upC z)P@~T%By_9j?bElkAe%}1GprOd~pqbjBitml=j^JIw*c9wn8^(3l=y}8APco?*9Uy CVfjq} delta 59 zcmZo@U~F)hAkE6cz`(#XQNf;(Wn;nuem-6xmzl4bfxnTTiLZHMVJRO|E%W4N{aU74 LCJ;5bQNJDlh(Hef diff --git a/ejercicios/beautifulsoup/ej4/data/src/__ssl.py b/ejercicios/beautifulsoup/ej4/data/src/__ssl.py deleted file mode 100644 index 475f052..0000000 --- a/ejercicios/beautifulsoup/ej4/data/src/__ssl.py +++ /dev/null @@ -1,5 +0,0 @@ -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 diff --git a/ejercicios/beautifulsoup/ej4/data/src/config.py b/ejercicios/beautifulsoup/ej4/data/src/config.py deleted file mode 100644 index ccde50e..0000000 --- a/ejercicios/beautifulsoup/ej4/data/src/config.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path - -BASE_URL = "https://www.recetasgratis.net" -RECIPES_URL = BASE_URL + "/Recetas-de-Aperitivos-tapas-listado_receta-1_1.html/" -DATA_DIR = Path(__file__).parent.parent / "data" -DB_PATH = DATA_DIR / "recetas.bd" \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/data/src/db.py b/ejercicios/beautifulsoup/ej4/data/src/db.py deleted file mode 100644 index 88bd2b4..0000000 --- a/ejercicios/beautifulsoup/ej4/data/src/db.py +++ /dev/null @@ -1,141 +0,0 @@ -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() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/data/src/main.py b/ejercicios/beautifulsoup/ej4/data/src/main.py deleted file mode 100644 index 39cd575..0000000 --- a/ejercicios/beautifulsoup/ej4/data/src/main.py +++ /dev/null @@ -1,85 +0,0 @@ -from bs4 import BeautifulSoup -import re -from tkinter import Tk -from tkinter import messagebox -import urllib.request -from datetime import datetime - -from db import DBManager, DBAttr -from ui import WinesUI -from __ssl import init_ssl -from config import * - -init_ssl() - -dbm = DBManager(DB_PATH) - -def create_tables(): - movies_attr = [ - DBAttr("title", "TEXT", "NOT NULL"), - DBAttr("original_title", "TEXT", "NOT NULL"), - DBAttr("country", "TEXT", "NOT NULL"), - DBAttr("date", "DATE", "NOT NULL"), - DBAttr("director", "TEXT", "NOT NULL"), - DBAttr("genres", "TEXT", "NOT NULL") - ] - - genres_attr = [ - DBAttr("genre", "TEXT") - ] - - dbm.create_table("movies", movies_attr) - dbm.create_table("genres", genres_attr) - -def parse_duration(duration): - duration.strip() - res = 0 - if duration[-1] == "h": - duration.replace("h","") - res = int(duration) * 60 - elif "h" in duration: - duration.replace("h","") - duration.replace("m","") - res = int(duration[0]) + int(duration[1:]) - else: - duration.replace("m","") - res = int(duration) - return res - -def main(): - create_tables() - root = Tk() - ui = WinesUI(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("movies") - dbm.clear("genres") - movies_count = persit_recetas() - ui.info(f"Hay {movies_count} recetas") - case "Recetas": - recetas = dbm.get_all("recetas") - ui.show_list(recetas, ["titulo", "dificultad", "comensales", "duracion", "autor", "fecha"]) - case "Receta por autor": - def search_title(title): - movies = [movie for movie in dbm.get_all("recetas") if title.lower() in movie["titulo"].lower()] - ui.show_list(recetas, ["titulo", "dificultad", "comensales", "duracion", "autor", "fecha"]) - ui.ask_text("Buscar por autor: ", search_title) - case "Receta por fecha": - def search_date(date): - d = datetime.strptime(date, "%d-%m-%Y") - movies = [movie for movie in dbm.get_all("movies") - if d < datetime.strptime(movie["date"], "%Y-%m-%d %H:%M:%S")] - ui.show_list(movies, ["title", "date"]) - ui.ask_text("Buscar por fecha: ", search_date) - - ui.callback = handle_action - root.mainloop() - dbm.close() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/src/main.py b/ejercicios/beautifulsoup/ej4/src/main.py index b9f10f4..3d0373c 100644 --- a/ejercicios/beautifulsoup/ej4/src/main.py +++ b/ejercicios/beautifulsoup/ej4/src/main.py @@ -7,7 +7,7 @@ from datetime import datetime import locale from db import DBManager, DBAttr -#from ui import RecipesUI +from ui import RecipesUI from __ssl import init_ssl from config import * @@ -28,53 +28,105 @@ def create_tables(): 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: - print(div) 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_=["property", "duracion"]).string.strip() if properties and properties.find("span", class_=["property", "duracion"]) else None - units = properties.find("span", class_=["property", "unidades"]).string.strip() if properties and properties.find("span", class_=["property", "unidades"]) else None + 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 + 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": duration, + "duration": parse_duration(duration), "author": author, "updated_at": updated_at }) - return dbm.count("recipes") + return dbm.count("recipes") - def main(): create_tables() - recipes_count = persist_recipes() - print(recipes_count) - #root = Tk() - #ui = RecipesUI(root) - - # def handle_action(action): - - #ui.callback = handle_action - #root.mainloop() - #dbm.close() - - print(dbm.get_all("recipes")) + 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() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/data/src/ui.py b/ejercicios/beautifulsoup/ej4/src/ui.py similarity index 97% rename from ejercicios/beautifulsoup/ej4/data/src/ui.py rename to ejercicios/beautifulsoup/ej4/src/ui.py index d1cc79a..423e8c1 100644 --- a/ejercicios/beautifulsoup/ej4/data/src/ui.py +++ b/ejercicios/beautifulsoup/ej4/src/ui.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk, messagebox from tkinter.scrolledtext import ScrolledText -class WinesUI(): +class RecipesUI(): def __init__(self, root, title = "AII"): self.root = root self.root.title(title) @@ -22,12 +22,12 @@ class WinesUI(): # 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