diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..21318f4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM postgres:15 + +# Variables de entorno útiles +ENV POSTGRES_USER=postgres \ + POSTGRES_DB=basededatos + +RUN apt-get update && \ +DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + gdal-bin \ + postgresql-15-pgvector \ + postgresql-15-postgis-3 \ + postgresql-15-postgis-3-scripts \ + proj-bin \ +&& rm -rf /var/lib/apt/lists/* + +# Copiamos scripts de inicialización +COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ diff --git a/Rag_de_textos.py b/Rag_de_textos.py new file mode 100644 index 0000000..4909dd9 --- /dev/null +++ b/Rag_de_textos.py @@ -0,0 +1,318 @@ +import marimo + +__generated_with = "0.14.17" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + return (mo,) + + +@app.cell +def _(): + import os + import textwrap + import yaml + import subprocess + from pathlib import Path + + # ==== Parámetros ==== + PUERTO_POSTGRES = 55432 + SERVICIO = "postgres_ext" + PASSWORD = "mipassword" + USER = "postgres" + DBNAME = "basededatos" + + # Lista de extensiones que quieres habilitar + EXTENSIONES = [ + # builtin + "hstore", "citext", "uuid-ossp", "pg_trgm", + # requieren paquetes + "postgis", "pgvector", + # "timescaledb" # <- si quieres usar base de timescaledb, activa la bandera abajo + ] + + # Usa imagen base de timescaledb cuando la extensión 'timescaledb' esté en la lista + timescaledb_base_image = False # pon True si quieres usar la imagen base de TimescaleDB + + RUTA_PROYECTO = Path(".").resolve() + + return ( + DBNAME, + EXTENSIONES, + PASSWORD, + PUERTO_POSTGRES, + Path, + RUTA_PROYECTO, + SERVICIO, + USER, + subprocess, + textwrap, + timescaledb_base_image, + yaml, + ) + + +@app.function +def pkgs_para_extensiones(exts, pg_major=15, use_timescale_base=False): + """ + Devuelve (pkgs_apt, builtins) para las extensiones solicitadas. + builtins = extensiones que no requieren apt + pkgs_apt = paquetes apt necesarios para otras extensiones + """ + builtins = [] + pkgs_apt = [] + + for e in exts: + e_low = e.lower() + if e_low in {"hstore", "citext", "uuid-ossp", "pg_trgm"}: + builtins.append(e_low) + elif e_low == "postgis": + pkgs_apt += [ + f"postgresql-{pg_major}-postgis-3", + f"postgresql-{pg_major}-postgis-3-scripts", + "gdal-bin", + "proj-bin", + ] + elif e_low == "pgvector": + pkgs_apt += [f"postgresql-{pg_major}-pgvector"] + elif e_low == "timescaledb": + if not use_timescale_base: + # Para instalar timescaledb necesitarías repos adicionales + pass + else: + raise ValueError(f"Extensión no soportada en este helper: {e}") + + pkgs_apt = sorted(set(pkgs_apt)) + return pkgs_apt, builtins + + +@app.cell +def _(DBNAME, Path, USER, textwrap): + def generar_dockerfile(ruta: Path, exts, use_timescale_base=False, pg_major=15): + ruta.mkdir(parents=True, exist_ok=True) + pkgs_apt, builtins = pkgs_para_extensiones(exts, pg_major, use_timescale_base) + + base_image = ( + f"timescale/timescaledb:2.16-pg{pg_major}" if use_timescale_base else f"postgres:{pg_major}" + ) + + apt_block = "" + if pkgs_apt: + apt_lines = [ + "RUN apt-get update && \\", + " DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\", + ] + # 👇 Backslash en **todas** las líneas de paquetes + for pkg in pkgs_apt: + apt_lines.append(f" {pkg} \\") + # y ya colgamos el rm del último paquete + apt_lines.append(" && rm -rf /var/lib/apt/lists/*") + apt_block = "\n".join(apt_lines) + + dockerfile = f""" + FROM {base_image} + + # Variables de entorno útiles + ENV POSTGRES_USER={USER} \\ + POSTGRES_DB={DBNAME} + + {apt_block} + + # Copiamos scripts de inicialización + COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ + """ + (ruta / "Dockerfile").write_text(textwrap.dedent(dockerfile).strip() + "\n", encoding="utf-8") + + return (generar_dockerfile,) + + +@app.cell +def _(Path): + def generar_init_sql(ruta: Path, exts): + init_dir = ruta / "docker-entrypoint-initdb.d" + init_dir.mkdir(parents=True, exist_ok=True) + + lines = ["-- Habilitar extensiones solicitadas"] + for e in exts: + e_low = e.lower() + ext_name = { + "pgvector": "vector", + "postgis": "postgis", + "hstore": "hstore", + "citext": "citext", + "uuid-ossp": "\"uuid-ossp\"", + "pg_trgm": "pg_trgm", + "timescaledb": "timescaledb", + }.get(e_low, e_low) + lines.append(f"CREATE EXTENSION IF NOT EXISTS {ext_name};") + + sql = "\n".join(lines) + "\n" + (init_dir / "10-extensions.sql").write_text(sql, encoding="utf-8") + + return (generar_init_sql,) + + +@app.cell +def _(DBNAME, Path, USER, yaml): + def crear_docker_compose(ruta: Path, servicio: str, puerto_host: int, password: str): + compose = { + "version": "3.8", + "services": { + servicio: { + "build": {"context": "."}, + "restart": "always", + "ports": [f"{puerto_host}:5432"], + "environment": { + "POSTGRES_PASSWORD": password, + "POSTGRES_USER": USER, + "POSTGRES_DB": DBNAME, + }, + "healthcheck": { + "test": ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"], + "interval": "10s", + "timeout": "5s", + "retries": 5, + }, + "volumes": [ + "postgres_data:/var/lib/postgresql/data" + ], + } + }, + "volumes": {"postgres_data": {}} + } + (ruta / "docker-compose.yml").write_text(yaml.dump(compose, sort_keys=False), encoding="utf-8") + + return (crear_docker_compose,) + + +@app.cell +def _(Path, subprocess): + def construir_y_levantar(ruta: Path): + def _run(cmd): + subprocess.run(cmd, cwd=ruta, check=True) + + try: + _run(["docker", "compose", "build"]) + _run(["docker", "compose", "up", "-d"]) + except Exception: + _run(["docker-compose", "build"]) + _run(["docker-compose", "up", "-d"]) + + return (construir_y_levantar,) + + +@app.cell +def _( + EXTENSIONES, + PASSWORD, + PUERTO_POSTGRES, + RUTA_PROYECTO, + SERVICIO, + construir_y_levantar, + crear_docker_compose, + generar_dockerfile, + generar_init_sql, + timescaledb_base_image, +): + if __name__ == "__main__": + RUTA_PROYECTO.mkdir(parents=True, exist_ok=True) + generar_dockerfile(RUTA_PROYECTO, EXTENSIONES, timescaledb_base_image) + generar_init_sql(RUTA_PROYECTO, EXTENSIONES) + crear_docker_compose(RUTA_PROYECTO, SERVICIO, PUERTO_POSTGRES, PASSWORD) + construir_y_levantar(RUTA_PROYECTO) + return + + +@app.cell +def _(PUERTO_POSTGRES): + from sqlalchemy import create_engine + from sqlalchemy.engine import Engine + from urllib.parse import quote_plus + + def conectar_postgres( + host: str = "localhost", + port: int = PUERTO_POSTGRES, + dbname: str = "basededatos", + user: str = "postgres", + password: str = "mipassword" + ) -> Engine: + """ + Devuelve un objeto SQLAlchemy Engine conectado a PostgreSQL. + Compatible con pandas.to_sql y read_sql. + """ + pwd = quote_plus(password) # Escapar caracteres especiales del password + url = f"postgresql+psycopg2://{user}:{pwd}@{host}:{port}/{dbname}" + + engine = create_engine( + url, + pool_size=5, + max_overflow=10, + future=True + ) + return engine + + return Engine, conectar_postgres + + +@app.cell +def _(Engine): + import pandas as pd + + def consultar_df(conn: Engine, query: str, params: dict | None = None) -> pd.DataFrame: + """ + Ejecuta una consulta SQL usando SQLAlchemy y devuelve los resultados como un DataFrame. + - conn: Engine devuelto por conectar_postgres() + - query: str con la consulta SQL + - params: dict opcional con parámetros de la consulta + """ + return pd.read_sql(query, con=conn, params=params) + + return (consultar_df,) + + +@app.cell +def _(conectar_postgres, consultar_df): + conn = conectar_postgres() + + query = """ + SELECT * + FROM information_schema.tables + + -- WHERE table_type = 'BASE TABLE' + -- AND table_schema NOT IN ('pg_catalog', 'information_schema') + + ORDER BY table_schema, table_name; + """ + + consultar_df(conn, query) + return (conn,) + + +@app.cell +def _(conn, mo): + _df = mo.sql( + f""" + SELECT * FROM information_schema.tables + """, + engine=conn + ) + return + + +@app.cell +def _(mo): + mo.md(r"""# Generacion de esquemas con sqlalchemy""") + return + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run() + diff --git a/__marimo__/session/Rag_de_textos.py.json b/__marimo__/session/Rag_de_textos.py.json new file mode 100644 index 0000000..a227945 --- /dev/null +++ b/__marimo__/session/Rag_de_textos.py.json @@ -0,0 +1,259 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.14.17" + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "Jbtt", + "code_hash": "fde985cc15f0093552fff888b240816c", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "plNZ", + "code_hash": "b2c1f26f82d1f970b9d43928212acde9", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "oZMf", + "code_hash": "b0b5cf4dac9ad1d2d8aa2d44ed0f20ce", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "mSjp", + "code_hash": "eccad4a6b0e5c3fd744d0203ad46d0a9", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "AUBn", + "code_hash": "5e55408f8b81b90732752beb11570e05", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "FHCW", + "code_hash": "11c9307c9267a2df95f9264fff9e1206", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "ybCH", + "code_hash": "a5bd6b98ad6169496877f80431f7c095", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "time=\"2025-08-19T15:16:04+02:00\" level=warning msg=\"/home/lucas/DataProyects/pdf_extraccion/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion\"\n" + }, + { + "type": "stream", + "name": "stdout", + "text": "#1 [internal] load local bake definitions\n" + }, + { + "type": "stream", + "name": "stdout", + "text": "#1 reading from stdin 569B done\n#1 DONE 0.0s\n" + }, + { + "type": "stream", + "name": "stdout", + "text": "\n#2 [internal] load build definition from Dockerfile\n#2 transferring dockerfile: 505B done\n#2 DONE 0.0s\n\n#3 [internal] load metadata for docker.io/library/postgres:15\n#3 DONE 0.0s\n\n#4 [internal] load .dockerignore\n#4 transferring context: 2B done\n#4 DONE 0.0s\n" + }, + { + "type": "stream", + "name": "stdout", + "text": "\n#5 [1/3] FROM docker.io/library/postgres:15@sha256:bc51cf4f1fe02cce7ed2370b20128a9b00b4eb804573a77d2a0d877aaa9c82b1\n#5 resolve docker.io/library/postgres:15@sha256:bc51cf4f1fe02cce7ed2370b20128a9b00b4eb804573a77d2a0d877aaa9c82b1 0.0s done\n#5 DONE 0.0s\n\n#6 [internal] load build context\n#6 transferring context: 401B done\n#6 DONE 0.0s\n\n#7 [2/3] RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gdal-bin postgresql-15-pgvector postgresql-15-postgis-3 postgresql-15-postgis-3-scripts proj-bin && rm -rf /var/lib/apt/lists/*\n#7 CACHED\n\n#8 [3/3] COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/\n#8 CACHED\n\n#9 exporting to image\n#9 exporting layers done\n#9 exporting manifest sha256:ddf2aa5c1127ef36f0583f757538ec9d9188c522cdf7192fd3c46873d403abb7 0.0s done\n#9 exporting config sha256:c2aa2387ddcb66d7f496ed5a6efd9be5da5cc966b3dcacfd23c58c1caefdf806 0.0s done\n#9 exporting attestation manifest sha256:7c1b9488e66ca92e2104f30369539c7fd0d9b9fb42f84ae97cc88cccbd118f86 0.0s done\n#9 exporting manifest list sha256:8f9fc603b962916902c09a3d1ecef8a7c50def8cb9346642e8a7c9ae2033fcf7\n" + }, + { + "type": "stream", + "name": "stdout", + "text": "#9 exporting manifest list sha256:8f9fc603b962916902c09a3d1ecef8a7c50def8cb9346642e8a7c9ae2033fcf7 0.0s done\n#9 naming to docker.io/library/pdf_extraccion-postgres_ext:latest done\n#9 unpacking to docker.io/library/pdf_extraccion-postgres_ext:latest done\n#9 DONE 0.1s\n\n#10 resolving provenance for metadata file\n" + }, + { + "type": "stream", + "name": "stdout", + "text": "#10 DONE 0.0s\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " pdf_extraccion-postgres_ext Built\n" + }, + { + "type": "stream", + "name": "stderr", + "text": "time=\"2025-08-19T15:16:05+02:00\" level=warning msg=\"/home/lucas/DataProyects/pdf_extraccion/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion\"\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " Network pdf_extraccion_default Creating\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " Network pdf_extraccion_default Created\n Volume \"pdf_extraccion_postgres_data\" Creating\n Volume \"pdf_extraccion_postgres_data\" Created\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " Container pdf_extraccion-postgres_ext-1 Creating\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " Container pdf_extraccion-postgres_ext-1 Created\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " Container pdf_extraccion-postgres_ext-1 Starting\n" + }, + { + "type": "stream", + "name": "stderr", + "text": " Container pdf_extraccion-postgres_ext-1 Started\n" + } + ] + }, + { + "id": "lHhD", + "code_hash": "0bb04e3bef0447e54fef3eac8f7f3117", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "kkmm", + "code_hash": "28ff78f15580f50ed3fc3cf6779825b6", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "uOeO", + "code_hash": "36e82227d7318e471f358e2772315864", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [] + }, + { + "id": "kNuO", + "code_hash": "6ffd13eedba1a63ac0a20ab64b8f7e8c", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [] + }, + { + "id": "PIZD", + "code_hash": "73a56a298e50653cdd8a2f3c836b0a13", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "

Generacion de esquemas con sqlalchemy

" + } + } + ], + "console": [] + }, + { + "id": "BWdf", + "code_hash": null, + "outputs": [], + "console": [] + } + ] +} \ No newline at end of file diff --git a/__marimo__/session/prompts_plantillas.py.json b/__marimo__/session/prompts_plantillas.py.json new file mode 100644 index 0000000..91411c9 --- /dev/null +++ b/__marimo__/session/prompts_plantillas.py.json @@ -0,0 +1,27 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.14.17" + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "LPNa", + "code_hash": null, + "outputs": [], + "console": [] + } + ] +} \ No newline at end of file diff --git a/__marimo__/session/prueba_excel_again.py.json b/__marimo__/session/prueba_excel_again.py.json new file mode 100644 index 0000000..3805305 --- /dev/null +++ b/__marimo__/session/prueba_excel_again.py.json @@ -0,0 +1,137 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.14.17" + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "MJUe", + "code_hash": "36193dfa033cd2be8e7b1bc67c72eb92", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "No se ha introducido ningun excel" + } + } + ], + "console": [] + }, + { + "id": "vblA", + "code_hash": "072dd5735109eb97e01dc1c8dd2ac33b", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "/home/lucas/DataProyects/pdf_extraccion/.venv/lib/python3.10/site-packages/openpyxl/styles/stylesheet.py:237: UserWarning: Workbook contains no default style, apply openpyxl's default\n warn(\"Workbook contains no default style, apply openpyxl's default\")\n" + } + ] + }, + { + "id": "bkHC", + "code_hash": "b73d872652e4a8e4937dec04d65b4456", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [] + }, + { + "id": "lEQa", + "code_hash": "3a6c948a1f859a38537bc684e5f430f5", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "
" + } + } + ], + "console": [] + }, + { + "id": "PKri", + "code_hash": "fa2b5880fdfd5de20113e8e50090e744", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "
Excel: Ventas_Diarias_AURGI_-310125.xlsxHoja: Informe
" + } + } + ], + "console": [] + }, + { + "id": "Xref", + "code_hash": "94bec97921afbb4d3f0d66d6359780b1", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [] + }, + { + "id": "SFPL", + "code_hash": "603dca0774218b4898894b77707d4173", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [] + }, + { + "id": "BYtC", + "code_hash": "83ccf5086147a1c5d5c6d80e90319b63", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "
" + } + } + ], + "console": [] + }, + { + "id": "tMil", + "code_hash": null, + "outputs": [], + "console": [] + } + ] +} \ No newline at end of file diff --git a/__marimo__/session/sqlalchemy_inicializacion.py.json b/__marimo__/session/sqlalchemy_inicializacion.py.json new file mode 100644 index 0000000..12207ab --- /dev/null +++ b/__marimo__/session/sqlalchemy_inicializacion.py.json @@ -0,0 +1,85 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.14.17" + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "MJUe", + "code_hash": "0df0bfd7ff72ff9c8e46d08b9cafe41f", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "

SQLAlchemy 1: Inicializaci\u00f3n del backend

" + } + } + ], + "console": [] + }, + { + "id": "vblA", + "code_hash": "431dc7f7109c8c3f501ddfb63cd22792", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "Aqui vamos a generar la Base y la session de el backend usando SQLAlchemy, que usar\u00e1n todos los objetos que se registren en la bbdd ademas de las capas de arquitecture_layer que hereden nuestras clases" + } + } + ], + "console": [] + }, + { + "id": "bkHC", + "code_hash": "1b2dbc3e0e1a520310bb37cab8c2b812", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stdout", + "text": "\u2139\ufe0f /home/lucas/DataProyects/pdf_extraccion/backend/.env ya tiene todas las claves necesarias.\n\u2705 Creado /home/lucas/DataProyects/pdf_extraccion/backend/db/base.py\n\u2705 Creado /home/lucas/DataProyects/pdf_extraccion/backend/db/session.py\n" + } + ] + }, + { + "id": "lEQa", + "code_hash": "8a9301f35990da46a5875fb8c2d57351", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stdout", + "text": "\u2705 /home/lucas/DataProyects/Snippets_marimo_noteebooks/utils/ArquitectureLayer/Repo.py \u2192 domains/arquitecture_layer/Repo.py\n\u2705 /home/lucas/DataProyects/Snippets_marimo_noteebooks/utils/ArquitectureLayer/Mapper.py \u2192 domains/arquitecture_layer/Mapper.py\n\u2705 /home/lucas/DataProyects/Snippets_marimo_noteebooks/utils/ArquitectureLayer/Model.py \u2192 domains/arquitecture_layer/Model.py\n\u2705 Creado domains/arquitecture_layer/__init__.py\n" + } + ] + } + ] +} \ No newline at end of file diff --git a/__marimo__/session/sqlalchemy_prompts_mmr.py.json b/__marimo__/session/sqlalchemy_prompts_mmr.py.json new file mode 100644 index 0000000..2739cf8 --- /dev/null +++ b/__marimo__/session/sqlalchemy_prompts_mmr.py.json @@ -0,0 +1,195 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.14.17" + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "MJUe", + "code_hash": "c4f05c85676b46ca0fe7e6ba2c7451cb", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "

SQLAlchemy 2: generacion de DB_MMR con LLMs

" + } + } + ], + "console": [] + }, + { + "id": "vblA", + "code_hash": "7869775eccc51f1498a040ec3dd84bd0", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "Aqui podremos hacer que el LLM pueda generar objetos y sus conexiones con la base de datos de forma que generemos un backend s\u00f3lido y consistente. La idea es que con esto generemos la plantilla que podamos pasarle al LLM para que \u00e9l genere el c\u00f3digo de manera organizada" + } + } + ], + "console": [] + }, + { + "id": "bkHC", + "code_hash": "e0801e8126ea6513ac1888571363f0c0", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "lEQa", + "code_hash": "1e0fb1180758d2b29ee2b3266ab63169", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "Definimos la entidad o entidades a generar y su descripcion para generar los campos usando un llm y su plantilla para la bbdd" + } + } + ], + "console": [] + }, + { + "id": "PKri", + "code_hash": "b34c8a33b26c85ee9344fb55d9b11301", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "Xref", + "code_hash": "030b2a7d1cf6c50221fdfed467780a24", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stdout", + "text": "\n\ud83d\udccc Nombre de dominio: **productos**\n\n\ud83d\udcdd Explicaci\u00f3n: \nItems para una tienda online\n\n---\n\ud83c\udfaf **Objetivo del prompt** \nGenera una lista de posibles campos para el modelo **productos**, en el siguiente formato:\n\n# Formato sugerido por l\u00ednea:\n# nombre_columna: tipo_python | tipo_sqlalchemy(opcional) | nullable={True/False} | default={valor/opcional} | comentarios\n# - Ya contienen : columnas sys_* (created_at, updated_at, etc.), __json__, etc.\n\nEjemplo esperado:\n\n```\n\nnombre: str | String(120) | nullable=False | comentarios=\"Nombre del productos\"\ndescripcion: str | Text | nullable=True | default=None | comentarios=\"Descripci\u00f3n opcional\"\nactivo: bool | Boolean | nullable=False | default=True | comentarios=\"Si el registro est\u00e1 activo\"\n\n```\n\n---\n\ud83d\udd01 Posibles relaciones en formato:\n```\n\n\"RELACIONES\": \"\",\n\n# Ej.: cliente_id -> FK a ventas.clientes.id (ondelete='RESTRICT'),\n\"RELACIONES\": \"usuario_id -> FK a seguridad.usuarios.id (ondelete='CASCADE')\"\n\n```\n\n---\n\ud83d\udcd1 Posibles \u00edndices y restricciones:\n```\n\n\"INDEX\\_LIST\": \"\", # Opcional\n\"UNIQUE\\_LIST\": \"\", # Opcional\n\n```\n\n\u26a1 Nota: Usa tu criterio para proponer campos, relaciones y restricciones que sean coherentes con el dominio **productos**.\n```\n" + } + ] + }, + { + "id": "yeSV", + "code_hash": "e26c00e88371c15203ce806dc5b41aeb", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "SFPL", + "code_hash": "47a5275bf6221951a2342125bca26d3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stdout", + "text": "Te paso mis clases base para que heredes correctamente:\n\n1) Model_base \n\n- Codigo de la clase Model: \n```\n# Model.py\nfrom sqlalchemy import Column, DateTime, String, Integer, Text, BigInteger, func\nfrom sqlalchemy.ext.declarative import declared_attr, as_declarative\nfrom datetime import datetime\nfrom backend.db.base import Base # tu Base declarativa\n\nclass Model_base(Base):\n __abstract__ = True\n\n # ID autoincremental por defecto en todos los modelos\n id = Column(BigInteger, primary_key=True, autoincrement=True)\n\n @declared_attr\n def sys_created_at(cls):\n # timestamptz + default en BD\n return Column(DateTime(timezone=True), server_default=func.now(), nullable=False)\n\n @declared_attr\n def sys_created_by(cls):\n return Column(String, nullable=True)\n\n @declared_attr\n def sys_updated_at(cls):\n # onupdate lo pone la BD al actualizar\n return Column(DateTime(timezone=True), onupdate=func.now(), nullable=True)\n\n @declared_attr\n def sys_updated_by(cls):\n return Column(String, nullable=True)\n\n @declared_attr\n def sys_version(cls):\n return Column(Integer, default=1, nullable=False)\n\n @declared_attr\n def sys_notes(cls):\n return Column(Text, nullable=True)\n\n @declared_attr\n def sys_deleted_at(cls):\n return Column(DateTime(timezone=True), nullable=True)\n\n def __repr__(self):\n id_val = getattr(self, \"id\", None)\n return f\"<{self.__class__.__name__} id={id_val}>\"\n\n def __str__(self):\n cls = self.__class__.__name__\n id_val = getattr(self, \"id\", None)\n return f\"{cls}(id={id_val})\"\n\n def __json__(self) -> dict:\n out = {}\n if not hasattr(self, \"__table__\"): return out\n for c in self.__table__.columns:\n v = getattr(self, c.name, None)\n out[c.name] = v.isoformat() if isinstance(v, datetime) else v\n return out\n```\n\n- Ruta de import: domains.arquitecture_layer.Repo import Repo_base\n- Contiene: columnas sys_* (created_at, updated_at, etc.), __json__, etc.\n\n2) Mapper_base\n\n- Codigo de la clase Model: \n```\n# Mapper.py\n\nfrom abc import ABC, abstractmethod\nfrom typing import TypeVar, Generic, Type\nimport json\n\nTDominio = TypeVar(\"TDominio\")\nTModelo = TypeVar(\"TModelo\")\n\nclass Mapper_base(ABC, Generic[TDominio, TModelo]):\n # ----------------------------\n # Conversiones individuales\n # ----------------------------\n\n @staticmethod\n @abstractmethod\n def to_model(obj: TDominio) -> TModelo:\n \"\"\"Convierte objeto de dominio a modelo ORM\"\"\"\n pass\n\n @staticmethod\n @abstractmethod\n def from_model(model: TModelo) -> TDominio:\n \"\"\"Convierte modelo ORM a objeto de dominio\"\"\"\n pass\n\n @staticmethod\n @abstractmethod\n def to_dict(obj: TDominio) -> dict:\n \"\"\"Convierte objeto de dominio a diccionario plano\"\"\"\n pass\n\n @staticmethod\n @abstractmethod\n def from_dict(d: dict) -> TDominio:\n \"\"\"Convierte diccionario plano a objeto de dominio\"\"\"\n pass\n\n @classmethod\n def to_json(cls, obj: TDominio) -> str:\n \"\"\"Convierte objeto de dominio a JSON string\"\"\"\n return json.dumps(cls.to_dict(obj), default=str)\n\n @classmethod\n def from_json(cls, json_str: str) -> TDominio:\n \"\"\"Convierte JSON string a objeto de dominio\"\"\"\n return cls.from_dict(json.loads(json_str))\n\n # ----------------------------\n # Conversiones en lote (bulk)\n # ----------------------------\n\n @classmethod\n def to_model_list(cls, objs: list[TDominio]) -> list[TModelo]:\n return [cls.to_model(o) for o in objs]\n\n @classmethod\n def from_model_list(cls, models: list[TModelo]) -> list[TDominio]:\n return [cls.from_model(m) for m in models]\n\n @classmethod\n def to_dict_list(cls, objs: list[TDominio]) -> list[dict]:\n return [cls.to_dict(o) for o in objs]\n\n @classmethod\n def from_dict_list(cls, dicts: list[dict]) -> list[TDominio]:\n return [cls.from_dict(d) for d in dicts]\n\n @classmethod\n def to_json_list(cls, objs: list[TDominio]) -> str:\n return json.dumps(cls.to_dict_list(objs), default=str)\n\n @classmethod\n def from_json_list(cls, json_str: str) -> list[TDominio]:\n return cls.from_dict_list(json.loads(json_str))\n```\n\n- Ruta de import: domains.arquitecture_layer.Mapper import Mapper_base\n\n3) Repo_base\n- Codigo de la clase Model: \n```\n# Repo.py\n\nfrom abc import ABC\nfrom typing import Type, TypeVar, Generic, Optional\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import func\nfrom datetime import datetime\nfrom datetime import datetime, timezone\n\nfrom .Mapper import Mapper_base # Aseg\u00farate de importar tu ABC base\n\nTModelo = TypeVar(\"TModelo\")\nTDominio = TypeVar(\"TDominio\")\n\nclass Repo_base(ABC, Generic[TModelo, TDominio]):\n def __init__(self, session: Session, modelo: type[TModelo], mapper: type[Mapper_base[TDominio, TModelo]]):\n self.session = session\n self.Modelo = modelo\n self.Mapper = mapper\n\n # ----------------------\n # ADD\n # ----------------------\n\n def add(self, dominio: TDominio, created_by: Optional[str] = None, notes: Optional[str] = None) -> str:\n data = self.Mapper.to_dict(dominio)\n data.update({\n \"sys_created_by\": created_by,\n \"sys_notes\": notes,\n \"sys_version\": 1\n })\n model = self.Modelo(**data)\n self.session.add(model)\n self.session.commit()\n return model.id\n\n def add_many(self, dominios: list[TDominio], created_by: Optional[str] = None, notes: Optional[str] = None) -> list[str]:\n ids = []\n for dominio in dominios:\n data = self.Mapper.to_dict(dominio)\n data.update({\n \"sys_created_by\": created_by,\n \"sys_notes\": notes,\n \"sys_version\": 1\n })\n model = self.Modelo(**data)\n self.session.add(model)\n ids.append(model.id)\n self.session.commit()\n return ids\n\n # ----------------------\n # GET\n # ----------------------\n\n def get_by_id(self, id_: str) -> Optional[TDominio]:\n model = self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first()\n return self.Mapper.from_model(model) if model else None\n\n def get_all(self) -> list[TDominio]:\n models = self.session.query(self.Modelo).filter_by(sys_deleted_at=None).all()\n return self.Mapper.from_model_list(models)\n\n def get_paginated(self, offset: int = 0, limit: int = 10) -> list[TDominio]:\n models = self.session.query(self.Modelo).filter_by(sys_deleted_at=None).offset(offset).limit(limit).all()\n return self.Mapper.from_model_list(models)\n\n def get_deleted(self) -> list[TDominio]:\n models = self.session.query(self.Modelo).filter(self.Modelo.sys_deleted_at.isnot(None)).all()\n return self.Mapper.from_model_list(models)\n\n # ----------------------\n # UPDATE\n # ----------------------\n\n def update(self, id_: str, new_data: dict, updated_by: Optional[str] = None, notes: Optional[str] = None) -> bool:\n model = self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first()\n if not model:\n return False\n\n for key, value in new_data.items():\n if hasattr(model, key):\n setattr(model, key, value)\n\n model.sys_updated_by = updated_by\n model.sys_notes = notes\n model.sys_version = (model.sys_version or 1) + 1\n self.session.commit()\n return True\n\n def bulk_update(self, updates: list[tuple[str, dict]], updated_by: Optional[str] = None, notes: Optional[str] = None) -> int:\n count = 0\n for id_, data in updates:\n if self.update(id_, data, updated_by=updated_by, notes=notes):\n count += 1\n return count\n\n # ----------------------\n # DELETE\n # ----------------------\n\n def delete_by_id(self, id_: str) -> bool:\n model = self.session.query(self.Modelo).filter_by(id=id_).first()\n if model:\n self.session.delete(model)\n self.session.commit()\n return True\n return False\n\n def delete_all(self) -> int:\n deleted = self.session.query(self.Modelo).delete()\n self.session.commit()\n return deleted\n\n # ----------------------\n # SOFT DELETE\n # ----------------------\n\n def soft_delete(self, id_: str, deleted_by: Optional[str] = None, notes: Optional[str] = None) -> bool:\n model = self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first()\n if model:\n model.sys_deleted_at = datetime.now(timezone.utc) # TZ-aware\n model.sys_updated_by = deleted_by\n model.sys_notes = notes\n model.sys_version = (model.sys_version or 1) + 1\n self.session.commit()\n return True\n return False\n\n def soft_restore(self, id_: str, restored_by: Optional[str] = None, notes: Optional[str] = None) -> bool:\n model = self.session.query(self.Modelo).filter_by(id=id_).first()\n if model and model.sys_deleted_at is not None:\n model.sys_deleted_at = None\n model.sys_updated_by = restored_by\n model.sys_notes = notes\n model.sys_version = (model.sys_version or 1) + 1\n self.session.commit()\n return True\n return False\n\n # ----------------------\n # OTROS\n # ----------------------\n\n def exists(self, id_: str) -> bool:\n return self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first() is not None\n\n def count(self) -> int:\n return self.session.query(self.Modelo).filter_by(sys_deleted_at=None).count()\n\n```\n\n- Ruta de import: from domains.arquitecture_layer.Model import Model_base\n\n\ud83e\udde9 Tarea:\nGenera en **un solo archivo** las 3 piezas siguientes para la entidad **productos**:\n- Dominio (dataclass): productosDom\n- Modelo ORM (SQLAlchemy): productosModel\n- Mapper concreto: productosMapper\n- Repo concreto (especializaci\u00f3n del gen\u00e9rico): productosRepo\n\nAqui puedes ver como hacerlo con este ejemplo: \n\n```python\nfrom pydantic import BaseModel, Field\n\nclass ItemIn(BaseModel):\n nombre: str = Field(min_length=1)\n\nclass ItemOut(ItemIn):\n id: int\n\n##############################\n\nfrom sqlalchemy import Column, String\nfrom backend.db.model_base import Model_base\n\nclass Item(Model_base):\n __tablename__ = \"item\" # opcional; si no, usa el nombre por defecto\n __table_args__ = {\"schema\": \"public\"}\n nombre = Column(String, nullable=False, index=True)\n\n##############################\n\nfrom domains.ejemplo.domain import ItemIn, ItemOut\nfrom domains.ejemplo.model import Item\n\nclass ItemMapper:\n @staticmethod\n def to_model(d: ItemIn) -> Item:\n return Item(nombre=d.nombre)\n\n @staticmethod\n def from_model(m: Item) -> ItemOut:\n return ItemOut(id=m.id, nombre=m.nombre)\n\n @staticmethod\n def to_dict(d: ItemIn) -> dict:\n return {\"nombre\": d.nombre}\n\n @staticmethod\n def from_dict(data: dict) -> ItemIn:\n return ItemIn(**data)\n\n\n#############################\n\n\nfrom typing import Optional\nfrom sqlalchemy.orm import Session\nfrom domains.ejemplo.model import Item\nfrom domains.ejemplo.mapper import ItemMapper\nfrom domains.arquitecture_layer.Repo import Repo_base # tu base\n\nclass ItemRepo(Repo_base[Item, \"ItemIn|ItemOut\"]):\n def __init__(self, session: Session):\n super().__init__(session=session, modelo=Item, mapper=ItemMapper)\n\n```\n\n\n\ud83e\uddf1 Esquema y tabla\n- schema: public (ej. \"ventas\")\n- __tablename__: productos (ej. \"clientes\")\n\n\ud83d\uddc2\ufe0f Campos (excluyendo sys_*, que ya aporta Model_base):\n\n\nid: int | Integer | nullable=False | comentarios=\"PK del producto\"\nsku: str | String(64) | nullable=False | comentarios=\"C\u00f3digo \u00fanico del producto\"\nnombre: str | String(200) | nullable=False | comentarios=\"Nombre del producto\"\ndescripcion: str | Text | nullable=True | default=None | comentarios=\"Descripci\u00f3n detallada\"\nprecio: Decimal | Numeric(12,2) | nullable=False | default=0.00 | comentarios=\"Precio base\"\nstock: int | Integer | nullable=False | default=0 | comentarios=\"Unidades disponibles\"\nactivo: bool | Boolean | nullable=False | default=True | comentarios=\"Si el producto est\u00e1 activo\"\n\n\n\n\ud83d\udd11 Clave primaria\n- Usa el ID autoincremental por defecto si est\u00e1 disponible. Si no, crea una columna id BigInteger autoincremental.\n\n\ud83d\udd17 Restricciones / \u00edndices (opcional)\n- uniques: uq_productos_sku (sku)\n- indexes: idx_productos_nombre (nombre); idx_productos_sku (sku)\n\n\ud83d\udd01 Relaciones (opcional)\n\n\n\ud83d\udd52 Zona horaria\n- Mant\u00e9n timestamps timezone-aware (ya lo hace Model_base).\n\n\ud83c\udfaf Reglas de salida / estilo\n- **Devuelve SOLO c\u00f3digo Python v\u00e1lido** (un \u00fanico archivo), sin comentarios fuera del c\u00f3digo ni explicaciones.\n- Importa exactamente desde mis rutas:\n from domains.arquitecture_layer.Repo import Repo_base import Model_base\n from domains.arquitecture_layer.Mapper import Mapper_base import Mapper_base\n from from domains.arquitecture_layer.Model import Model_base import Repo_base\n- Tipado fuerte (Python 3.10+), `from __future__ import annotations` opcional.\n- Dominio como `@dataclass` -> `productosDom`.\n- Modelo ORM hereda de `Model_base` y (si existe) `IdIntMixin`.\n- Usa `__table_args__ = {\"schema\": \"public\"}`.\n- Tipos SQLAlchemy razonables (String(N), Integer, Numeric(p,s), Boolean, DateTime, JSON, etc.).\n- En `Mapper`:\n - `to_model`, `from_model`, `to_dict`, `from_dict` (respetando tipos; Numeric -> float en dominio).\n- `Repo`:\n - Clase `productosRepo(Repo_base[productosModel, productosDom])` con `__init__(self, session)` que fija `Modelo` y `Mapper`.\n - A\u00f1ade 1\u20133 m\u00e9todos de consulta comunes (ej.: `get_by_nombre`, `buscar_por_rango_gasto`, paginado por pa\u00eds), usando `self.session`.\n- No generes `create_all` ni conexi\u00f3n.\n- No dupliques columnas sys_*.\n- Mant\u00e9n nombre de columnas igual a las llaves del dominio y del dict del mapper.\n\n\ud83d\udce6 Salida esperada (estructura del archivo):\n- imports\n- dataclass de dominio `productosDom`\n- clase ORM `productosModel`\n- clase `productosMapper`\n- clase `productosRepo`\n\n\nGenera ahora el archivo `.py` final.\n" + } + ] + }, + { + "id": "RGSE", + "code_hash": "a1204eb54a53c4ed520511cd637e8d2a", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "

Conexion con bbdd

" + } + } + ], + "console": [] + }, + { + "id": "Kclp", + "code_hash": "d19ff2ccfb6db6a077b4575908247605", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stdout", + "text": "\u2714 Tablas creadas\n" + } + ] + }, + { + "id": "emfo", + "code_hash": "efa869609408ddb6b406bf9d23dcd7bc", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "

Falta por generar los bloques de codigos o prompt/templates que me ayuden a generar los usos con Repo y la logica para el Dominio

" + } + } + ], + "console": [] + }, + { + "id": "BYtC", + "code_hash": "87ee1ef84a53699c468cc44edb65111c", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + } + ] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5edc732 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' +services: + postgres_ext: + build: + context: . + restart: always + ports: + - 55432:5432 + environment: + POSTGRES_PASSWORD: mipassword + POSTGRES_USER: postgres + POSTGRES_DB: basededatos + healthcheck: + test: + - CMD-SHELL + - pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} + interval: 10s + timeout: 5s + retries: 5 + volumes: + - postgres_data:/var/lib/postgresql/data +volumes: + postgres_data: {} diff --git a/docker-entrypoint-initdb.d/10-extensions.sql b/docker-entrypoint-initdb.d/10-extensions.sql new file mode 100644 index 0000000..d63eb53 --- /dev/null +++ b/docker-entrypoint-initdb.d/10-extensions.sql @@ -0,0 +1,7 @@ +-- Habilitar extensiones solicitadas +CREATE EXTENSION IF NOT EXISTS hstore; +CREATE EXTENSION IF NOT EXISTS citext; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS vector;