diff --git a/.vscode/settings.json b/.vscode/settings.json index 642ff51..e4da00a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "python.REPL.enableREPLSmartSend": false + "python.REPL.enableREPLSmartSend": false, + "python-envs.defaultEnvManager": "ms-python.python:venv" } \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/__ssl.py b/ejercicios/beautifulsoup/ej1/src/__ssl.py deleted file mode 100644 index 475f052..0000000 --- a/ejercicios/beautifulsoup/ej1/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/ej1/src/config.py b/ejercicios/beautifulsoup/ej1/src/config.py deleted file mode 100644 index 4248462..0000000 --- a/ejercicios/beautifulsoup/ej1/src/config.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path - -URL = "https://www.vinissimus.com/es/vinos/tinto/?cursor=" -DATA_DIR = Path(__file__).parent.parent / "data" -CSV_PATH = DATA_DIR / "books.csv" -DB_PATH = DATA_DIR / "books.bd" \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/db.py b/ejercicios/beautifulsoup/ej1/src/db.py deleted file mode 100644 index 88bd2b4..0000000 --- a/ejercicios/beautifulsoup/ej1/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/ej1/src/main.py b/ejercicios/beautifulsoup/ej1/src/main.py deleted file mode 100644 index a501b2f..0000000 --- a/ejercicios/beautifulsoup/ej1/src/main.py +++ /dev/null @@ -1,110 +0,0 @@ -from bs4 import BeautifulSoup -import re -from tkinter import Tk -from tkinter import messagebox -import urllib.request - -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(): - wines_attr = [ - DBAttr("name", "TEXT", "NOT NULL"), - DBAttr("price", "INTEGER", "NOT NULL"), - DBAttr("origin", "TEXT", "NOT NULL"), - DBAttr("cellar", "TEXT", "NOT NULL"), - DBAttr("type", "TEXT", "NOT NULL") - ] - - types_attr = [ - DBAttr("type", "TEXT") - ] - - dbm.create_table("wines", wines_attr) - dbm.create_table("types", types_attr) - -def extract_wines(): - l = [] - - for i in range(0,3): - f = urllib.request.urlopen(URL+str(i*36)) - doc = BeautifulSoup(f, "lxml") - page = doc.find_all("div", class_="product-list-item") - l.extend(page) - - return l - -def persist_wines(wines): - types = set() - - for wine in wines: - details = wine.find("div",class_=["details"]) - name = details.a.h2.string.strip() - price = list(wine.find("p",class_=["price"]).stripped_strings)[0] - origin = details.find("div",class_=["region"]).string.strip() - cellar = details.find("div", class_=["cellar-name"]).string.strip() - grapes = "".join(details.find("div",class_=["tags"]).stripped_strings) - for g in grapes.split("/"): - types.add(g.strip()) - disc = wine.find("p",class_=["price"]).find_next_sibling("p",class_="dto") - if disc: - price = list(disc.stripped_strings)[0] - - dbm.insert("wines", {"name": name, "price": float(price.replace(',', '.')), "origin": origin, "cellar": cellar, "type": grapes}) - - for type in types: - dbm.insert("types", {"type": type}) - - return dbm.count("wines"), dbm.count("types") - -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("wines") - dbm.clear("types") - wines = extract_wines() - wines_count, types_count = persist_wines(wines) - ui.info(f"Hay {wines_count} vinos y {types_count} uvas.") - case "listar": - wines = dbm.get_all("wines") - ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) - case "buscar_denominacion": - origins = list({wine["origin"] for wine in dbm.get_all("wines")}) - origins.sort() - def search_origin(origin): - wines = [wine for wine in dbm.get_all("wines") if wine["origin"] == origin] - ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) - ui.ask_spinbox("Buscar por denominación: ", origins, search_origin) - case "buscar_precio": - def search_price(price): - wines = [wine for wine in dbm.get_all("wines") if float(wine["price"]) <= float(price)] - wines.sort(key=lambda w: float(w["price"])) - ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) - ui.ask_text("Selecciona precio: ", search_price) - case "buscar_uva": - types = [t for t in dbm.get_singleton("types")] - types.sort() - def search_type(type): - wines = [wine for wine in dbm.get_all("wines") if type in wine["type"]] - ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) - ui.ask_spinbox("Selecciona tip de uva: ", types, search_type) - - ui.callback = handle_action - root.mainloop() - dbm.close() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/src/ui.py b/ejercicios/beautifulsoup/ej1/src/ui.py deleted file mode 100644 index 1de6395..0000000 --- a/ejercicios/beautifulsoup/ej1/src/ui.py +++ /dev/null @@ -1,76 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox -from tkinter.scrolledtext import ScrolledText - -class WinesUI(): - 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_command(label="Listar", command=lambda: self.callback("listar")) - datos_menu.add_separator() - datos_menu.add_command(label="Salir", command=self.root.quit) - self.menu.add_cascade(label="Datos", menu=datos_menu) - - # Menu Buscar - buscar_menu = tk.Menu(self.menu, tearoff=0) - buscar_menu.add_command(label="Denominación", command=lambda: self.callback("buscar_denominacion")) - buscar_menu.add_command(label="Precio", command=lambda: self.callback("buscar_precio")) - buscar_menu.add_command(label="Uva", command=lambda: self.callback("buscar_uva")) - 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) \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej2/src/__ssl.py b/ejercicios/beautifulsoup/ej2/src/__ssl.py deleted file mode 100644 index 475f052..0000000 --- a/ejercicios/beautifulsoup/ej2/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/ej2/src/config.py b/ejercicios/beautifulsoup/ej2/src/config.py deleted file mode 100644 index 7a42b6e..0000000 --- a/ejercicios/beautifulsoup/ej2/src/config.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path - -BASE_URL = "https://www.elseptimoarte.net" -ESTRENOS_URL = BASE_URL + "/estrenos/2025/" -DATA_DIR = Path(__file__).parent.parent / "data" -DB_PATH = DATA_DIR / "movies.bd" \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej2/src/db.py b/ejercicios/beautifulsoup/ej2/src/db.py deleted file mode 100644 index 88bd2b4..0000000 --- a/ejercicios/beautifulsoup/ej2/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/ej2/src/main.py b/ejercicios/beautifulsoup/ej2/src/main.py deleted file mode 100644 index ec4c85a..0000000 --- a/ejercicios/beautifulsoup/ej2/src/main.py +++ /dev/null @@ -1,109 +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 persist_movies(): - f = urllib.request.urlopen(ESTRENOS_URL) - bs = BeautifulSoup(f, "lxml") - list_items = bs.find("ul", class_="elements").find_all("li") - for li in list_items: - f = urllib.request.urlopen(BASE_URL+li.a['href']) - bs = BeautifulSoup(f, "lxml") - data = bs.find("main", class_="informativo").find("section",class_="highlight").div.dl - original_title = data.find("dt", string=lambda s: s and "Título original" in s).find_next_sibling("dd").get_text(strip=True) - country = "".join(data.find("dt", string=lambda s: s and "País" in s).find_next_sibling("dd").stripped_strings) - title = data.find("dt", string=lambda s: s and "Título" in s).find_next_sibling("dd").get_text(strip=True) - date = datetime.strptime(data.find("dt",string="Estreno en España").find_next_sibling("dd").string.strip(), '%d/%m/%Y') - - genres_director = bs.find("div",id="datos_pelicula") - genres_str = genres_director.find("p", class_="categorias").get_text(strip=True) - genres_list = [g.strip() for g in genres_str.split(",") if g.strip()] - for g in genres_list: - existing = dbm.exists("genres", "genre", g) - if not existing: - dbm.insert("genres", {"genre": g}) - director = "".join(genres_director.find("p",class_="director").stripped_strings) - - dbm.insert("movies", { - "title": title, - "original_title": original_title, - "country": country, - "date": date, - "director": director, - "genres": genres_str - }) - - return dbm.count("movies"), dbm.count("genres") - -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, genres_count = persist_movies() - ui.info(f"Hay {movies_count} películas y {genres_count} géneros") - case "listar": - movies = dbm.get_all("movies") - ui.show_list(movies, ["title", "original_title", "country", "date", "director", "genres"]) - case "buscar_titulo": - def search_title(title): - movies = [movie for movie in dbm.get_all("movies") if title.lower() in movie["title"].lower()] - ui.show_list(movies, ["title", "country", "director"]) - ui.ask_text("Buscar por titulo: ", search_title) - case "buscar_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) - case "buscar_genero": - genres = [g for g in dbm.get_singleton("genres")] - genres.sort() - def search_genre(genre): - movies = [movie for movie in dbm.get_all("movies") if genre in movie["genres"]] - ui.show_list(movies, ["title", "date"]) - ui.ask_spinbox("Selecciona género: ", genres, search_genre) - - ui.callback = handle_action - root.mainloop() - dbm.close() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej2/src/ui.py b/ejercicios/beautifulsoup/ej2/src/ui.py deleted file mode 100644 index 0d50a5e..0000000 --- a/ejercicios/beautifulsoup/ej2/src/ui.py +++ /dev/null @@ -1,76 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox -from tkinter.scrolledtext import ScrolledText - -class WinesUI(): - 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_command(label="Listar", command=lambda: self.callback("listar")) - datos_menu.add_separator() - datos_menu.add_command(label="Salir", command=self.root.quit) - self.menu.add_cascade(label="Datos", menu=datos_menu) - - # Menu Buscar - buscar_menu = tk.Menu(self.menu, tearoff=0) - buscar_menu.add_command(label="Título", command=lambda: self.callback("buscar_titulo")) - buscar_menu.add_command(label="Fecha", command=lambda: self.callback("buscar_fecha")) - buscar_menu.add_command(label="Género", command=lambda: self.callback("buscar_genero")) - 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) \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/src/__ssl.py b/ejercicios/beautifulsoup/ej4/src/__ssl.py deleted file mode 100644 index 475f052..0000000 --- a/ejercicios/beautifulsoup/ej4/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/src/config.py b/ejercicios/beautifulsoup/ej4/src/config.py deleted file mode 100644 index c76e2b6..0000000 --- a/ejercicios/beautifulsoup/ej4/src/config.py +++ /dev/null @@ -1,6 +0,0 @@ -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" \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/src/db.py b/ejercicios/beautifulsoup/ej4/src/db.py deleted file mode 100644 index 88bd2b4..0000000 --- a/ejercicios/beautifulsoup/ej4/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/src/main.py b/ejercicios/beautifulsoup/ej4/src/main.py deleted file mode 100644 index 3d0373c..0000000 --- a/ejercicios/beautifulsoup/ej4/src/main.py +++ /dev/null @@ -1,132 +0,0 @@ -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() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/src/ui.py b/ejercicios/beautifulsoup/ej4/src/ui.py deleted file mode 100644 index 423e8c1..0000000 --- a/ejercicios/beautifulsoup/ej4/src/ui.py +++ /dev/null @@ -1,79 +0,0 @@ -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) \ No newline at end of file diff --git a/ejercicios/python/ej2/src/db.py b/ejercicios/python/ej2/src/db.py deleted file mode 100644 index 31bf2cb..0000000 --- a/ejercicios/python/ej2/src/db.py +++ /dev/null @@ -1,124 +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_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 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/python/ej2/src/files.py b/ejercicios/python/ej2/src/files.py deleted file mode 100644 index 1b66710..0000000 --- a/ejercicios/python/ej2/src/files.py +++ /dev/null @@ -1,23 +0,0 @@ -import csv - -class FileReader: - _instance = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self, delimiter=";"): - self.delimiter = delimiter - - def read(self, file): - results = [] - try: - with open(file, encoding="utf-8-sig") as f: - reader = csv.DictReader(f, delimiter=self.delimiter) - for row in reader: - results.append(dict(row)) - except Exception as e: - print(f"Error leyendo archivo: {e}") - return results \ No newline at end of file diff --git a/ejercicios/python/ej2/src/main.py b/ejercicios/python/ej2/src/main.py deleted file mode 100644 index e8123bc..0000000 --- a/ejercicios/python/ej2/src/main.py +++ /dev/null @@ -1,77 +0,0 @@ -from pathlib import Path -from tkinter import Tk - -from files import FileReader -from db import DBManager, DBAttr -from ui import BooksUI - -DATA_DIR = Path(__file__).parent.parent / "data" -CSV_PATH = DATA_DIR / "books.csv" -DB_PATH = DATA_DIR / "books.bd" - -dbm = DBManager(DB_PATH) -fr = FileReader() - -def create_tables(): - book_attrs = [ - DBAttr("isbn", "INTEGER", "PRIMARY KEY"), - DBAttr("title", "TEXT", "NOT NULL"), - DBAttr("author", "TEXT"), - DBAttr("year", "DATE"), - DBAttr("publisher", "TEXT") - ] - - dbm.create_table("books", book_attrs) - -def main(): - create_tables() - root = Tk() - ui = BooksUI(root) - - def handle_action(action): - match(action): - case "cargar": - books = fr.read(CSV_PATH) - count = 0 - for book in books: - book["isbn"] = int(book["isbn"]) - if not dbm.exists("books", "isbn", book["isbn"]): - dbm.insert("books", book) - count += 1 - ui.info(f"{count} libros almacenados.") - case "listar_todo": - books = dbm.get_all("books") - ui.show_list(books, ["isbn", "title", "author", "year"]) - case "listar_ordenado": - def sort(attr): - books = dbm.get_all("books") - def key_fn(x): - v = x[attr] - if isinstance(v, int): - return v - elif isinstance(v, str) and v.isdigit(): - return int(v) - else: - return float('inf') - books.sort(key=key_fn) - ui.show_list(books, ["isbn", "title", "author", "year"]) - ui.ask_radiobutton("Ordenar por: ", ["isbn", "year"], sort) - case "buscar_titulo": - def search_title(title): - books = [book for book in dbm.get_all("books") if title.lower() in book["title"].lower()] - ui.show_list(books, ["isbn", "title", "author", "year"]) - ui.ask_text("Buscar por título: ", search_title) - case "buscar_editorial": - publishers = list({book["publisher"] for book in dbm.get_all("books")}) - publishers.sort() - def search_publisher(publisher): - books = [book for book in dbm.get_all("books") if book["publisher"] == publisher] - ui.show_list(books, ["title", "author", "publisher"]) - ui.ask_spinbox("Selecciona editorial: ", publishers, search_publisher) - - ui.callback = handle_action - root.mainloop() - dbm.close() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ejercicios/python/ej2/src/ui.py b/ejercicios/python/ej2/src/ui.py deleted file mode 100644 index c0358cc..0000000 --- a/ejercicios/python/ej2/src/ui.py +++ /dev/null @@ -1,80 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox -from tkinter.scrolledtext import ScrolledText - -class BooksUI(): - 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="Completo", command=lambda: self.callback("listar_completo")) - listar_menu.add_command(label="Ordenado", command=lambda: self.callback("listar_ordenado")) - self.menu.add_cascade(label="Listar", menu=listar_menu) - - # Menu Buscar - buscar_menu = tk.Menu(self.menu, tearoff=0) - buscar_menu.add_command(label="Título", command=lambda: self.callback("buscar_titulo")) - buscar_menu.add_command(label="Editorial", command=lambda: self.callback("buscar_editorial")) - self.menu.add_cascade(label="Buscar", menu=buscar_menu) - - # Callback externo desde el punto de entrada - self.callback = None - - def show_list(self, books, 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 book in books: - row = " | ".join(str(book[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) \ No newline at end of file diff --git a/whoosh/main.py b/exercises/information_retrieval/ej4/index/RecipesIndex_WRITELOCK old mode 100644 new mode 100755 similarity index 100% rename from whoosh/main.py rename to exercises/information_retrieval/ej4/index/RecipesIndex_WRITELOCK diff --git a/exercises/information_retrieval/ej4/index/RecipesIndex_t6wx2alah0vo0oou.seg b/exercises/information_retrieval/ej4/index/RecipesIndex_t6wx2alah0vo0oou.seg new file mode 100644 index 0000000..b40b63c Binary files /dev/null and b/exercises/information_retrieval/ej4/index/RecipesIndex_t6wx2alah0vo0oou.seg differ diff --git a/exercises/information_retrieval/ej4/index/RecipesIndex_wspm4h2lgo13lp79.seg b/exercises/information_retrieval/ej4/index/RecipesIndex_wspm4h2lgo13lp79.seg new file mode 100644 index 0000000..4949c2b Binary files /dev/null and b/exercises/information_retrieval/ej4/index/RecipesIndex_wspm4h2lgo13lp79.seg differ diff --git a/exercises/information_retrieval/ej4/index/_RecipesIndex_2.toc b/exercises/information_retrieval/ej4/index/_RecipesIndex_2.toc new file mode 100644 index 0000000..4f8959a Binary files /dev/null and b/exercises/information_retrieval/ej4/index/_RecipesIndex_2.toc differ diff --git a/exercises/information_retrieval/ej4/main.py b/exercises/information_retrieval/ej4/main.py new file mode 100644 index 0000000..59e73a9 --- /dev/null +++ b/exercises/information_retrieval/ej4/main.py @@ -0,0 +1,247 @@ +import locale +import re +import urllib.request +from datetime import datetime +from pathlib import Path +import tkinter as tk +from tkinter import messagebox, ttk +from tkinter import Tk +from tkinter.scrolledtext import ScrolledText +import shutil, re, os + +from bs4 import BeautifulSoup +from whoosh.index import create_in,open_dir +from whoosh.fields import Schema, TEXT, DATETIME, KEYWORD, ID, NUMERIC +from whoosh.qparser import QueryParser +from whoosh import index, qparser, query + +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 / "index" + +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 + +def create_index(): + if not os.path.exists(DATA_DIR): + os.mkdir(DATA_DIR) + + if not index.exists_in(DATA_DIR, indexname="RecipesIndex"): + schema = Schema( + title=TEXT(stored=True), + difficulty=TEXT(stored=True), + duration=TEXT(stored=True), + units=NUMERIC(stored=True, numtype=int), + author=ID(stored=True), + updated_at=DATETIME(stored=True), + features=KEYWORD(stored=True, commas=True), + intro=TEXT(stored=True) + ) + idx = create_in(DATA_DIR, schema=schema, indexname="RecipesIndex") + print(f"Created index: {idx.indexname}") + else: + print(f"An index already exists") + +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(): + idx = index.open_dir(DATA_DIR, "RecipesIndex") + writer = idx.writer() + count = 0 + 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 = div.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 "Unknown" + intro = div.find("div", class_="intro").get_text() + properties = div.find("div", class_="properties") + duration = properties.find("span", class_="duracion").string.strip() if properties and properties.find("span", class_="duracion") else "Unknown" + units = int(properties.find("span", class_="unidades").string.strip()) if properties and properties.find("span", class_="unidades") else -1 + 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") + features = bs2.find("div", class_=["properties", "inline"]).get_text(strip=True).replace("Características adicionales:", "") if bs2.find("div", class_=["properties", "inline"]) else "Unknown" + + writer.add_document( + title=title, + difficulty=difficulty, + duration=duration, + units=units, + author=author, + updated_at=updated_at, + features=features, + intro=intro + ) + + count += 1 + + writer.commit() + + return count + +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("load")) + datos_menu.add_command(label="Listar", command=lambda: self.callback("list_recipes")) + datos_menu.add_separator() + datos_menu.add_command(label="Salir", command=self.root.quit) + self.menu.add_cascade(label="Datos", menu=datos_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Título o Introducción", command=lambda: self.callback("search_title_or_intro")) + buscar_menu.add_command(label="Fecha", command=lambda: self.callback("search_updated_at")) + buscar_menu.add_command(label="Características y Título", command=lambda: self.callback("search_features_and_title")) + 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.get(field, "Unknown")) 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) + +def main(): + init_ssl() + locale.setlocale(locale.LC_TIME, "es_ES.UTF-8") + + create_index() + root = Tk() + ui = RecipesUI(root) + + def handle_action(action): + match(action): + case "load": + resp = messagebox.askyesno(title="Cargar", message="Quieres cargar todos los datos de nuevo?") + if resp: + recipes_count = persist_recipes() + ui.info(f"Se han indexado {recipes_count} recetas") + case "list_recipes": + ix = open_dir(DATA_DIR, "RecipesIndex") + with ix.searcher() as searcher: + recipes = searcher.search(query.Every(), limit=None) + clear = [] + for r in recipes: + d = dict(r) + clear.append(d) + print(clear) + ui.show_list(clear, ["title", "difficulty", "units", "duration"]) + # case "search_title_or_intro": + # 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 "search_updated_at": + # 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) + # case "search_features_and_title": + # 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) + + ui.callback = handle_action + root.mainloop() + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/ejercicios/python/ej1/src/main.py b/exercises/python_basics/ej1/src/main.py similarity index 92% rename from ejercicios/python/ej1/src/main.py rename to exercises/python_basics/ej1/src/main.py index ebce6b3..cfb57d0 100644 --- a/ejercicios/python/ej1/src/main.py +++ b/exercises/python_basics/ej1/src/main.py @@ -1,6 +1,7 @@ import requests import re +# --- CONSTANTS ------------------------------------------ RSS_URL = "https://www.abc.es/rss/2.0/espana/andalucia/" ITEM_PATTERN = r"(.*?)" MONTHS = { @@ -9,6 +10,7 @@ MONTHS = { "Sep": "09", "Oct": "10", "Nov": "11", "Dec": "12", } +# --- MAIN PROGRAM FUNCTIONS ----------------------------- def get_tag(text, tag): m = re.search(rf"<{tag}>(.*?)", text, re.DOTALL) return m.group(1).strip() if m else None diff --git a/ejercicios/python/ej2/data/books.bd b/exercises/python_basics/ej2/data/books.bd similarity index 100% rename from ejercicios/python/ej2/data/books.bd rename to exercises/python_basics/ej2/data/books.bd diff --git a/ejercicios/python/ej2/data/books.csv b/exercises/python_basics/ej2/data/books.csv similarity index 100% rename from ejercicios/python/ej2/data/books.csv rename to exercises/python_basics/ej2/data/books.csv diff --git a/exercises/python_basics/ej2/main.py b/exercises/python_basics/ej2/main.py new file mode 100644 index 0000000..04f9b05 --- /dev/null +++ b/exercises/python_basics/ej2/main.py @@ -0,0 +1,300 @@ +from pathlib import Path +from tkinter import Tk +import sqlite3 +import csv +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText + +# --- CONSTANTS ------------------------------------------ +DATA_DIR = Path(__file__).parent / "data" +CSV_PATH = DATA_DIR / "books.csv" +DB_PATH = DATA_DIR / "books.bd" + +# --- HELPER CLASSES ------------------------------------- +class FileReader: + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, delimiter=";"): + self.delimiter = delimiter + + def read(self, file): + results = [] + try: + with open(file, encoding="utf-8-sig") as f: + reader = csv.DictReader(f, delimiter=self.delimiter) + for row in reader: + results.append(dict(row)) + except Exception as e: + print(f"Error leyendo archivo: {e}") + return results + +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_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 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() + +class BooksUI(): + 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="Completo", command=lambda: self.callback("listar_completo")) + listar_menu.add_command(label="Ordenado", command=lambda: self.callback("listar_ordenado")) + self.menu.add_cascade(label="Listar", menu=listar_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Título", command=lambda: self.callback("buscar_titulo")) + buscar_menu.add_command(label="Editorial", command=lambda: self.callback("buscar_editorial")) + self.menu.add_cascade(label="Buscar", menu=buscar_menu) + + # Callback externo desde el punto de entrada + self.callback = None + + def show_list(self, books, 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 book in books: + row = " | ".join(str(book[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) + +# --- MAIN PROGRAM FUNCTIONS ----------------------------- +dbm = DBManager(DB_PATH) +fr = FileReader() + +def create_tables(): + book_attrs = [ + DBAttr("isbn", "INTEGER", "PRIMARY KEY"), + DBAttr("title", "TEXT", "NOT NULL"), + DBAttr("author", "TEXT"), + DBAttr("year", "DATE"), + DBAttr("publisher", "TEXT") + ] + + dbm.create_table("books", book_attrs) + +def main(): + create_tables() + root = Tk() + ui = BooksUI(root) + + def handle_action(action): + match(action): + case "cargar": + books = fr.read(CSV_PATH) + count = 0 + for book in books: + book["isbn"] = int(book["isbn"]) + if not dbm.exists("books", "isbn", book["isbn"]): + dbm.insert("books", book) + count += 1 + ui.info(f"{count} libros almacenados.") + case "listar_todo": + books = dbm.get_all("books") + ui.show_list(books, ["isbn", "title", "author", "year"]) + case "listar_ordenado": + def sort(attr): + books = dbm.get_all("books") + def key_fn(x): + v = x[attr] + if isinstance(v, int): + return v + elif isinstance(v, str) and v.isdigit(): + return int(v) + else: + return float('inf') + books.sort(key=key_fn) + ui.show_list(books, ["isbn", "title", "author", "year"]) + ui.ask_radiobutton("Ordenar por: ", ["isbn", "year"], sort) + case "buscar_titulo": + def search_title(title): + books = [book for book in dbm.get_all("books") if title.lower() in book["title"].lower()] + ui.show_list(books, ["isbn", "title", "author", "year"]) + ui.ask_text("Buscar por título: ", search_title) + case "buscar_editorial": + publishers = list({book["publisher"] for book in dbm.get_all("books")}) + publishers.sort() + def search_publisher(publisher): + books = [book for book in dbm.get_all("books") if book["publisher"] == publisher] + ui.show_list(books, ["title", "author", "publisher"]) + ui.ask_spinbox("Selecciona editorial: ", publishers, search_publisher) + + ui.callback = handle_action + root.mainloop() + dbm.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej1/data/books.bd b/exercises/web_scrapping/ej1/data/books.bd similarity index 100% rename from ejercicios/beautifulsoup/ej1/data/books.bd rename to exercises/web_scrapping/ej1/data/books.bd diff --git a/exercises/web_scrapping/ej1/main.py b/exercises/web_scrapping/ej1/main.py new file mode 100644 index 0000000..423218a --- /dev/null +++ b/exercises/web_scrapping/ej1/main.py @@ -0,0 +1,332 @@ +from bs4 import BeautifulSoup +import re +from tkinter import Tk +from tkinter import messagebox +import urllib.request +from pathlib import Path +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText +import sqlite3 + +# --- CONSTANTS ------------------------------------------ +URL = "https://www.vinissimus.com/es/vinos/tinto/?cursor=" +DATA_DIR = Path(__file__).parent / "data" +CSV_PATH = DATA_DIR / "books.csv" +DB_PATH = DATA_DIR / "books.bd" + +# --- HELPER CLASSES ------------------------------------- +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() + +class WinesUI(): + 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_command(label="Listar", command=lambda: self.callback("listar")) + datos_menu.add_separator() + datos_menu.add_command(label="Salir", command=self.root.quit) + self.menu.add_cascade(label="Datos", menu=datos_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Denominación", command=lambda: self.callback("buscar_denominacion")) + buscar_menu.add_command(label="Precio", command=lambda: self.callback("buscar_precio")) + buscar_menu.add_command(label="Uva", command=lambda: self.callback("buscar_uva")) + 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) + +# --- MAIN PROGRAM FUNCTIONS ----------------------------- +dbm = DBManager(DB_PATH) + +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 + +def create_tables(): + wines_attr = [ + DBAttr("name", "TEXT", "NOT NULL"), + DBAttr("price", "INTEGER", "NOT NULL"), + DBAttr("origin", "TEXT", "NOT NULL"), + DBAttr("cellar", "TEXT", "NOT NULL"), + DBAttr("type", "TEXT", "NOT NULL") + ] + + types_attr = [ + DBAttr("type", "TEXT") + ] + + dbm.create_table("wines", wines_attr) + dbm.create_table("types", types_attr) + +def extract_wines(): + l = [] + + for i in range(0,3): + f = urllib.request.urlopen(URL+str(i*36)) + doc = BeautifulSoup(f, "lxml") + page = doc.find_all("div", class_="product-list-item") + l.extend(page) + + return l + +def persist_wines(wines): + types = set() + + for wine in wines: + details = wine.find("div",class_=["details"]) + name = details.a.h2.string.strip() + price = list(wine.find("p",class_=["price"]).stripped_strings)[0] + origin = details.find("div",class_=["region"]).string.strip() + cellar = details.find("div", class_=["cellar-name"]).string.strip() + grapes = "".join(details.find("div",class_=["tags"]).stripped_strings) + for g in grapes.split("/"): + types.add(g.strip()) + disc = wine.find("p",class_=["price"]).find_next_sibling("p",class_="dto") + if disc: + price = list(disc.stripped_strings)[0] + + dbm.insert("wines", {"name": name, "price": float(price.replace(',', '.')), "origin": origin, "cellar": cellar, "type": grapes}) + + for type in types: + dbm.insert("types", {"type": type}) + + return dbm.count("wines"), dbm.count("types") + +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("wines") + dbm.clear("types") + wines = extract_wines() + wines_count, types_count = persist_wines(wines) + ui.info(f"Hay {wines_count} vinos y {types_count} uvas.") + case "listar": + wines = dbm.get_all("wines") + ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) + case "buscar_denominacion": + origins = list({wine["origin"] for wine in dbm.get_all("wines")}) + origins.sort() + def search_origin(origin): + wines = [wine for wine in dbm.get_all("wines") if wine["origin"] == origin] + ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) + ui.ask_spinbox("Buscar por denominación: ", origins, search_origin) + case "buscar_precio": + def search_price(price): + wines = [wine for wine in dbm.get_all("wines") if float(wine["price"]) <= float(price)] + wines.sort(key=lambda w: float(w["price"])) + ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) + ui.ask_text("Selecciona precio: ", search_price) + case "buscar_uva": + types = [t for t in dbm.get_singleton("types")] + types.sort() + def search_type(type): + wines = [wine for wine in dbm.get_all("wines") if type in wine["type"]] + ui.show_list(wines, ["name", "price", "origin", "cellar", "type"]) + ui.ask_spinbox("Selecciona tip de uva: ", types, search_type) + + ui.callback = handle_action + root.mainloop() + dbm.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej2/data/movies.bd b/exercises/web_scrapping/ej2/data/movies.bd similarity index 100% rename from ejercicios/beautifulsoup/ej2/data/movies.bd rename to exercises/web_scrapping/ej2/data/movies.bd diff --git a/exercises/web_scrapping/ej2/main.py b/exercises/web_scrapping/ej2/main.py new file mode 100644 index 0000000..922a468 --- /dev/null +++ b/exercises/web_scrapping/ej2/main.py @@ -0,0 +1,330 @@ +from bs4 import BeautifulSoup +import re +import urllib.request +from datetime import datetime +from pathlib import Path +import sqlite3 +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText +from tkinter import Tk + +# --- CONSTANTS ------------------------------------------ +BASE_URL = "https://www.elseptimoarte.net" +ESTRENOS_URL = BASE_URL + "/estrenos/2025/" +DATA_DIR = Path(__file__).parent / "data" +DB_PATH = DATA_DIR / "movies.bd" + +# --- HELPER CLASSES ------------------------------------- +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() + +class WinesUI(): + 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_command(label="Listar", command=lambda: self.callback("listar")) + datos_menu.add_separator() + datos_menu.add_command(label="Salir", command=self.root.quit) + self.menu.add_cascade(label="Datos", menu=datos_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Título", command=lambda: self.callback("buscar_titulo")) + buscar_menu.add_command(label="Fecha", command=lambda: self.callback("buscar_fecha")) + buscar_menu.add_command(label="Género", command=lambda: self.callback("buscar_genero")) + 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) + +# --- MAIN PROGRAM FUNCTIONS ----------------------------- +dbm = DBManager(DB_PATH) + +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 + +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 persist_movies(): + f = urllib.request.urlopen(ESTRENOS_URL) + bs = BeautifulSoup(f, "lxml") + list_items = bs.find("ul", class_="elements").find_all("li") + for li in list_items: + f = urllib.request.urlopen(BASE_URL+li.a['href']) + bs = BeautifulSoup(f, "lxml") + data = bs.find("main", class_="informativo").find("section",class_="highlight").div.dl + original_title = data.find("dt", string=lambda s: s and "Título original" in s).find_next_sibling("dd").get_text(strip=True) + country = "".join(data.find("dt", string=lambda s: s and "País" in s).find_next_sibling("dd").stripped_strings) + title = data.find("dt", string=lambda s: s and "Título" in s).find_next_sibling("dd").get_text(strip=True) + date = datetime.strptime(data.find("dt",string="Estreno en España").find_next_sibling("dd").string.strip(), '%d/%m/%Y') + + genres_director = bs.find("div",id="datos_pelicula") + genres_str = genres_director.find("p", class_="categorias").get_text(strip=True) + genres_list = [g.strip() for g in genres_str.split(",") if g.strip()] + for g in genres_list: + existing = dbm.exists("genres", "genre", g) + if not existing: + dbm.insert("genres", {"genre": g}) + director = "".join(genres_director.find("p",class_="director").stripped_strings) + + dbm.insert("movies", { + "title": title, + "original_title": original_title, + "country": country, + "date": date, + "director": director, + "genres": genres_str + }) + + return dbm.count("movies"), dbm.count("genres") + +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, genres_count = persist_movies() + ui.info(f"Hay {movies_count} películas y {genres_count} géneros") + case "listar": + movies = dbm.get_all("movies") + ui.show_list(movies, ["title", "original_title", "country", "date", "director", "genres"]) + case "buscar_titulo": + def search_title(title): + movies = [movie for movie in dbm.get_all("movies") if title.lower() in movie["title"].lower()] + ui.show_list(movies, ["title", "country", "director"]) + ui.ask_text("Buscar por titulo: ", search_title) + case "buscar_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) + case "buscar_genero": + genres = [g for g in dbm.get_singleton("genres")] + genres.sort() + def search_genre(genre): + movies = [movie for movie in dbm.get_all("movies") if genre in movie["genres"]] + ui.show_list(movies, ["title", "date"]) + ui.ask_spinbox("Selecciona género: ", genres, search_genre) + + 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/recipes.bd b/exercises/web_scrapping/ej4/data/recipes.bd similarity index 100% rename from ejercicios/beautifulsoup/ej4/data/recipes.bd rename to exercises/web_scrapping/ej4/data/recipes.bd diff --git a/exercises/web_scrapping/ej4/main.py b/exercises/web_scrapping/ej4/main.py new file mode 100644 index 0000000..e7cfa15 --- /dev/null +++ b/exercises/web_scrapping/ej4/main.py @@ -0,0 +1,356 @@ +from bs4 import BeautifulSoup +import re +import urllib.request +from datetime import datetime +import locale +from pathlib import Path +from tkinter import Tk +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText +import sqlite3 + +# --- CONSTANTS ------------------------------------------ +BASE_URL = "https://recetas.elperiodico.com" +RECIPES_URL = BASE_URL + "/Recetas-de-Aperitivos-tapas-listado_receta-1_1.html" +DATA_DIR = Path(__file__).parent / "data" +DB_PATH = DATA_DIR / "recipes.bd" + +# --- HELPER CLASSES ------------------------------------- +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() + +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) + +# --- MAIN PROGRAM FUNCTIONS ----------------------------- +dbm = DBManager(DB_PATH) + +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 + +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(): + locale.setlocale(locale.LC_TIME, "es_ES.UTF-8") + 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() \ No newline at end of file