diff --git a/Dockerfile b/Dockerfile index 21318f4..623d5d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,11 +6,7 @@ ENV POSTGRES_USER=postgres \ 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 diff --git a/Generacion de bbdd.py b/Generacion de bbdd.py new file mode 100644 index 0000000..a647b20 --- /dev/null +++ b/Generacion de bbdd.py @@ -0,0 +1,220 @@ +import marimo + +__generated_with = "0.15.5" +app = marimo.App(width="columns") + + +@app.cell +def _(): + import os + import textwrap + import yaml + import subprocess + from pathlib import Path + + # ==== Parámetros ==== + PUERTO_POSTGRES = 55455 + 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 + + +if __name__ == "__main__": + app.run() diff --git a/__marimo__/session/Crud de notas.py.json b/__marimo__/session/Crud de notas.py.json new file mode 100644 index 0000000..2e5742f --- /dev/null +++ b/__marimo__/session/Crud de notas.py.json @@ -0,0 +1,306 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.15.5" + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "MJUe", + "code_hash": "a40093b1686243c3c7cfe538710216ac", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "vblA", + "code_hash": "e01ef05d140a83f20a1373bbc701f873", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stdout", + "text": "\u2139\ufe0f /home/lucas/DataProyects/myrag/backend/.env ya tiene todas las claves necesarias.\n\u2705 Creado /home/lucas/DataProyects/myrag/backend/db/base.py\n\u2705 Creado /home/lucas/DataProyects/myrag/backend/db/session.py\n" + } + ] + }, + { + "id": "bkHC", + "code_hash": "9a66ffddaebf2517492a50f3cec46737", + "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" + } + ] + }, + { + "id": "lEQa", + "code_hash": "151311bdb1d09f9b69b11b735701f9e4", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "PKri", + "code_hash": "0ec08d2138de5b6833360c8a32834465", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "
{\n "CAMPOS": [\n "columna: tipo_python | tipo_sqlalchemy | nullable={True/False} | default={valor/opcional} | comentarios='texto'"\n ],\n "RELACIONES": [\n "campo_1 -> FK a public.campo_2 (ondelete='CASCADE/RESTRICT/SET NULL')"\n ],\n "INDEX_LIST": [\n "nombre_indice"\n ],\n "UNIQUE_LIST": [\n "nombre_unico"\n ]\n}\ntrue/false/null en min\u00fascula si incluyes valores l\u00f3gicos o nulos.nombre: str | String(120) | nullable=False | comentarios="Nombre del "\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# Ejemplos.: \ncliente_id -> FK a public.clientes.id (ondelete='RESTRICT'),\nusuario_id -> FK a public.usuarios.id (ondelete='CASCADE')\n\n---\n\n"RELACIONES": "",\n"INDEX\\_LIST": "", # Opcional\n"UNIQUE\\_LIST": "", # Opcional\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# 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# 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()\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"} # Usa el schema al final para evitar el error SchemaItem' object, such as a 'Column' or a 'Constraint' expected\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)\nfrom __future__ import annotations opcional.\n- Dominio No puede ser @dataclass -> Dom.\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 Repo(Repo_base[Model, Dom]) 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\ud83d\udce6 Salida esperada (estructura del archivo):\n- imports\n- dataclass de dominio Dom\n- clase ORM Model\n- clase Mapper\n- clase Repo\nGenera ahora el archivo .py final.'/home/lucas/DataProyects/myrag/.model/nomic-embed-text-v1.5'" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "\rFetching 20 files: 0%| | 0/20 [00:00, ?it/s]" + }, + { + "type": "stream", + "name": "stderr", + "text": "\rFetching 20 files: 5%|\u2588\u2588\u2588\u2588\u258d | 1/20 [00:00<00:06, 2.80it/s]" + }, + { + "type": "stream", + "name": "stderr", + "text": "\rFetching 20 files: 30%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u258d | 6/20 [00:03<00:07, 1.78it/s]\rFetching 20 files: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 20/20 [00:03<00:00, 6.03it/s]\n" + } + ] + }, + { + "id": "RGSE", + "code_hash": "e64c8b56fa9ae281a52adbd60011ac93", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "
Traceback (most recent call last):\n File "/home/lucas/DataProyects/myrag/.venv/lib/python3.10/site-packages/marimo/_runtime/executor.py", line 138, in execute_cell\n exec(cell.body, glbls)\n File "/tmp/marimo_87281/__marimo__cell_iLit_.py", line 5, in <module>\n engine=conn2\nNameError: name 'conn2' is not defined. Did you mean: 'conn'?\n{\n "CAMPOS": [\n "columna: tipo_python | tipo_sqlalchemy | nullable={True/False} | default={valor/opcional} | comentarios='texto'"\n ],\n "RELACIONES": [\n "campo_1 -> FK a public.campo_2 (ondelete='CASCADE/RESTRICT/SET NULL')"\n ],\n "INDEX_LIST": [\n "nombre_indice"\n ],\n "UNIQUE_LIST": [\n "nombre_unico"\n ]\n}\ntrue/false/null en min\u00fascula si incluyes valores l\u00f3gicos o nulos.nombre: str | String(120) | nullable=False | comentarios="Nombre del prueba_simple"\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# Ejemplos.: \ncliente_id -> FK a public.clientes.id (ondelete='RESTRICT'),\nusuario_id -> FK a public.usuarios.id (ondelete='CASCADE')\n\n---\n\n"RELACIONES": "",\n"INDEX\\_LIST": "", # Opcional\n"UNIQUE\\_LIST": "", # Opcional\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# 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# 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()\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"} # Usa el schema al final para evitar el error SchemaItem' object, such as a 'Column' or a 'Constraint' expected\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)\nfrom __future__ import annotations opcional.\n- Dominio No puede ser @dataclass -> prueba_simpleDom.\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 prueba_simpleRepo(Repo_base[prueba_simpleModel, prueba_simpleDom]) 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\ud83d\udce6 Salida esperada (estructura del archivo):\n- imports\n- dataclass de dominio prueba_simpleDom\n- clase ORM prueba_simpleModel\n- clase prueba_simpleMapper\n- clase prueba_simpleRepo\nGenera ahora el archivo .py final.```python\nfrom sqlalchemy import Column, String, Text, Boolean\nfrom sqlalchemy.orm import Session\nfrom domains.arquitecture_layer.Model import Model_base\nfrom domains.arquitecture_layer.Mapper import Mapper_base\nfrom domains.arquitecture_layer.Repo import Repo_base\n\nclass prueba_simpleDom:\n def __init__(self, nombre: str, descripcion: str = None, activo: bool = True):\n self.nombre = nombre\n self.descripcion = descripcion\n self.activo = activo\n\nclass prueba_simpleModel(Model_base):\n __tablename__ = "prueba_simple"\n __table_args__ = (\n {"schema": "public"}\n )\n\n nombre = Column(String(120), nullable=False, comment='Nombre del prueba_simple')\n descripcion = Column(Text, nullable=True, default=None, comment='Descripci\u00f3n opcional')\n activo = Column(Boolean, nullable=False, default=True, comment='Si el registro est\u00e1 activo')\n\nclass prueba_simpleMapper(Mapper_base[prueba_simpleDom, prueba_simpleModel]):\n @staticmethod\n def to_model(obj: prueba_simpleDom) -> prueba_simpleModel:\n return prueba_simpleModel(\n nombre=obj.nombre,\n descripcion=obj.descripcion,\n activo=obj.activo\n )\n\n @staticmethod\n def from_model(model: prueba_simpleModel) -> prueba_simpleDom:\n return prueba_simpleDom(\n nombre=model.nombre,\n descripcion=model.descripcion,\n activo=model.activo\n )\n\n @staticmethod\n def to_dict(obj: prueba_simpleDom) -> dict:\n return {\n "nombre": obj.nombre,\n "descripcion": obj.descripcion,\n "activo": obj.activo\n }\n\n @staticmethod\n def from_dict(data: dict) -> prueba_simpleDom:\n return prueba_simpleDom(\n nombre=data["nombre"],\n descripcion=data.get("descripcion"),\n activo=data.get("activo", True)\n )\n\nclass prueba_simpleRepo(Repo_base[prueba_simpleModel, prueba_simpleDom]):\n def __init__(self, session: Session):\n super().__init__(session=session, modelo=prueba_simpleModel, mapper=prueba_simpleMapper)\n\n def get_by_nombre(self, nombre: str) -> list[prueba_simpleDom]:\n models = self.session.query(self.Modelo).filter_by(nombre=nombre, sys_deleted_at=None).all()\n return self.Mapper.from_model_list(models)\n\n def get_activos(self) -> list[prueba_simpleDom]:\n models = self.session.query(self.Modelo).filter_by(activo=True, sys_deleted_at=None).all()\n return self.Mapper.from_model_list(models)\n\n def get_inactivos(self) -> list[prueba_simpleDom]:\n models = self.session.query(self.Modelo).filter_by(activo=False, sys_deleted_at=None).all()\n return self.Mapper.from_model_list(models)\n1024,w&&(t.dump&&G2===t.dump.charCodeAt(0)?S+="?":S+="? "),S+=t.dump,w&&(S+=ZU(t,e)),Rd(t,e+1,E,!0,w)&&(t.dump&&G2===t.dump.charCodeAt(0)?S+=":":S+=": ",S+=t.dump,a+=S));t.tag=n,t.dump=a||"{}"}function _te(t,e,r){var s,a,n,c,f,p;for(a=r?t.explicitTypes:t.implicitTypes,n=0,c=a.length;n {var n,c;if(Object.getPrototypeOf(s).toString()==="[object Set]")if(typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");let f=[...s],p=[...s];if(!r(p,Object.assign(Object.assign({},a),{coercion:void 0})))return!1;let h=()=>p.some((E,w)=>E!==f[w])?new Set(p):s;return a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",K2(a.coercion,s,h)]),!0}else{let f=!0;for(let p of s)if(f=t(p,Object.assign({},a))&&f,!f&&a?.errors==null)break;return f}if(typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");let f={value:s};return r(s,Object.assign(Object.assign({},a),{coercion:Wf(f,"value")}))?(a.coercions.push([(c=a.p)!==null&&c!==void 0?c:".",K2(a.coercion,s,()=>new Set(f.value))]),!0):!1}return mr(a,`Expected a set (got ${ti(s)})`)}})}function PWe(t,e){let r=kx(Qx([t,e])),s=Rx(e,{keys:t});return Wr({test:(a,n)=>{var c,f,p;if(Object.getPrototypeOf(a).toString()==="[object Map]")if(typeof n?.coercions<"u"){if(typeof n?.coercion>"u")return mr(n,"Unbound coercion result");let h=[...a],E=[...a];if(!r(E,Object.assign(Object.assign({},n),{coercion:void 0})))return!1;let w=()=>E.some((S,x)=>S[0]!==h[x][0]||S[1]!==h[x][1])?new Map(E):a;return n.coercions.push([(c=n.p)!==null&&c!==void 0?c:".",K2(n.coercion,a,w)]),!0}else{let h=!0;for(let[E,w]of a)if(h=t(E,Object.assign({},n))&&h,!h&&n?.errors==null||(h=e(w,Object.assign(Object.assign({},n),{p:s0(n,E)}))&&h,!h&&n?.errors==null))break;return h}if(typeof n?.coercions<"u"){if(typeof n?.coercion>"u")return mr(n,"Unbound coercion result");let h={value:a};return Array.isArray(a)?r(a,Object.assign(Object.assign({},n),{coercion:void 0}))?(n.coercions.push([(f=n.p)!==null&&f!==void 0?f:".",K2(n.coercion,a,()=>new Map(h.value))]),!0):!1:s(a,Object.assign(Object.assign({},n),{coercion:Wf(h,"value")}))?(n.coercions.push([(p=n.p)!==null&&p!==void 0?p:".",K2(n.coercion,a,()=>new Map(Object.entries(h.value)))]),!0):!1}return mr(n,`Expected a map (got ${ti(a)})`)}})}function Qx(t,{delimiter:e}={}){let r=Dre(t.length);return Wr({test:(s,a)=>{var n;if(typeof s=="string"&&typeof e<"u"&&typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");s=s.split(e),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,s)])}if(!Array.isArray(s))return mr(a,`Expected a tuple (got ${ti(s)})`);let c=r(s,Object.assign({},a));for(let f=0,p=s.length;f {var n;if(Array.isArray(s)&&typeof a?.coercions<"u")return typeof a?.coercion>"u"?mr(a,"Unbound coercion result"):r(s,Object.assign(Object.assign({},a),{coercion:void 0}))?(s=Object.fromEntries(s),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,s)]),!0):!1;if(typeof s!="object"||s===null)return mr(a,`Expected an object (got ${ti(s)})`);let c=Object.keys(s),f=!0;for(let p=0,h=c.length;p `:`[${x}]`)}s.push(...this.arity.leading.map(c=>`<${c}>`)),this.arity.extra===Hl?s.push("..."):s.push(...this.arity.extra.map(c=>`[${c}]`)),s.push(...this.arity.trailing.map(c=>`<${c}>`))}return{usage:s.join(" "),options:a}}compile(){if(typeof this.context>"u")throw new Error("Assertion failed: No context attached");let e=Pre(),r=En.InitialNode,s=this.usage().usage,a=this.options.filter(f=>f.required).map(f=>f.nameSet);r=Ou(e,_l()),Ea(e,En.InitialNode,ei.StartOfInput,r,["setCandidateState",{candidateUsage:s,requiredOptions:a}]);let n=this.arity.proxy?"always":"isNotOptionLike",c=this.paths.length>0?this.paths:[[]];for(let f of c){let p=r;if(f.length>0){let S=Ou(e,_l());BE(e,p,S),this.registerOptions(e,S),p=S}for(let S=0;S1)r=e;else if(this.head)s=this.head.next,r=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var a=0;s!==null;a++)r=t(r,s.value,a),s=s.next;return r};Fn.prototype.reduceReverse=function(t,e){var r,s=this.tail;if(arguments.length>1)r=e;else if(this.tail)s=this.tail.prev,r=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(var a=this.length-1;s!==null;a--)r=t(r,s.value,a),s=s.prev;return r};Fn.prototype.toArray=function(){for(var t=new Array(this.length),e=0,r=this.head;r!==null;e++)t[e]=r.value,r=r.next;return t};Fn.prototype.toArrayReverse=function(){for(var t=new Array(this.length),e=0,r=this.tail;r!==null;e++)t[e]=r.value,r=r.prev;return t};Fn.prototype.slice=function(t,e){e=e||this.length,e<0&&(e+=this.length),t=t||0,t<0&&(t+=this.length);var r=new Fn;if(e0&&(Ae=p.slice(0,E),p=p.slice(E),w-=E),ye&&I===!0&&w>0?(ye=p.slice(0,w),se=p.slice(w)):I===!0?(ye="",se=p):ye=p,ye&&ye!==""&&ye!=="/"&&ye!==p&&Toe(ye.charCodeAt(ye.length-1))&&(ye=ye.slice(0,-1)),r.unescape===!0&&(se&&(se=koe.removeBackslashes(se)),ye&&W===!0&&(ye=koe.removeBackslashes(ye)));let X={prefix:Ae,input:t,start:E,base:ye,glob:se,isBrace:S,isBracket:x,isGlob:I,isExtglob:T,isGlobstar:N,negated:ee,negatedExtglob:ie};if(r.tokens===!0&&(X.maxDepth=0,Toe(pe)||c.push(Be),X.tokens=c),r.parts===!0||r.tokens===!0){let De;for(let Te=0;Te("lookup"in r||(r.lookup=this.lookup),e[pI](r,s))}uninstall(e){if(Due(e),e[pI]){if(e[eH]!==this)throw new Error("The agent is not owned by this CacheableLookup instance");e.createConnection=e[pI],delete e[pI],delete e[eH]}}updateInterfaceInfo(){let{_iface:e}=this;this._iface=bue(),(e.has4&&!this._iface.has4||e.has6&&!this._iface.has6)&&this._cache.clear()}clear(e){if(e){this._cache.delete(e);return}this._cache.clear()}};tH.exports=MQ;tH.exports.default=MQ});var Tue=_((XLt,rH)=>{"use strict";var xet=typeof URL>"u"?Ie("url").URL:URL,ket="text/plain",Qet="us-ascii",Que=(t,e)=>e.some(r=>r instanceof RegExp?r.test(t):r===t),Ret=(t,{stripHash:e})=>{let r=t.match(/^data:([^,]*?),([^#]*?)(?:#(.*))?$/);if(!r)throw new Error(`Invalid URL: ${t}`);let s=r[1].split(";"),a=r[2],n=e?"":r[3],c=!1;s[s.length-1]==="base64"&&(s.pop(),c=!0);let f=(s.shift()||"").toLowerCase(),h=[...s.map(E=>{let[w,S=""]=E.split("=").map(x=>x.trim());return w==="charset"&&(S=S.toLowerCase(),S===Qet)?"":`${w}${S?`=${S}`:""}`}).filter(Boolean)];return c&&h.push("base64"),(h.length!==0||f&&f!==ket)&&h.unshift(f),`data:${h.join(";")},${c?a.trim():a}${n?`#${n}`:""}`},Rue=(t,e)=>{if(e={defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...e},Reflect.has(e,"normalizeHttps"))throw new Error("options.normalizeHttps is renamed to options.forceHttp");if(Reflect.has(e,"normalizeHttp"))throw new Error("options.normalizeHttp is renamed to options.forceHttps");if(Reflect.has(e,"stripFragment"))throw new Error("options.stripFragment is renamed to options.stripHash");if(t=t.trim(),/^data:/i.test(t))return Ret(t,e);let r=t.startsWith("//");!r&&/^\.*\//.test(t)||(t=t.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,e.defaultProtocol));let a=new xet(t);if(e.forceHttp&&e.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(e.forceHttp&&a.protocol==="https:"&&(a.protocol="http:"),e.forceHttps&&a.protocol==="http:"&&(a.protocol="https:"),e.stripAuthentication&&(a.username="",a.password=""),e.stripHash&&(a.hash=""),a.pathname&&(a.pathname=a.pathname.replace(/((?!:).|^)\/{2,}/g,(n,c)=>/^(?!\/)/g.test(c)?`${c}/`:"/")),a.pathname&&(a.pathname=decodeURI(a.pathname)),e.removeDirectoryIndex===!0&&(e.removeDirectoryIndex=[/^index\.[a-z]+$/]),Array.isArray(e.removeDirectoryIndex)&&e.removeDirectoryIndex.length>0){let n=a.pathname.split("/"),c=n[n.length-1];Que(c,e.removeDirectoryIndex)&&(n=n.slice(0,n.length-1),a.pathname=n.slice(1).join("/")+"/")}if(a.hostname&&(a.hostname=a.hostname.replace(/\.$/,""),e.stripWWW&&/^www\.([a-z\-\d]{2,63})\.([a-z.]{2,5})$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\./,""))),Array.isArray(e.removeQueryParameters))for(let n of[...a.searchParams.keys()])Que(n,e.removeQueryParameters)&&a.searchParams.delete(n);return e.sortQueryParameters&&a.searchParams.sort(),e.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\/$/,"")),t=a.toString(),(e.removeTrailingSlash||a.pathname==="/")&&a.hash===""&&(t=t.replace(/\/$/,"")),r&&!e.normalizeProtocol&&(t=t.replace(/^http:\/\//,"//")),e.stripProtocol&&(t=t.replace(/^(?:https?:)?\/\//,"")),t};rH.exports=Rue;rH.exports.default=Rue});var Oue=_(($Lt,Nue)=>{Nue.exports=Fue;function Fue(t,e){if(t&&e)return Fue(t)(e);if(typeof t!="function")throw new TypeError("need wrapper function");return Object.keys(t).forEach(function(s){r[s]=t[s]}),r;function r(){for(var s=new Array(arguments.length),a=0;a{"use strict";var cst=SI(),u0e=CT(),GI=Ie("fs"),ust=jI(),c0e=Ie("path"),rG=TI();A0e.exports=(t,e,r)=>{typeof t=="function"?(r=t,e=null,t={}):Array.isArray(t)&&(e=t,t={}),typeof e=="function"&&(r=e,e=null),e?e=Array.from(e):e=[];let s=cst(t);if(s.sync&&typeof r=="function")throw new TypeError("callback not supported for sync tar functions");if(!s.file&&typeof r=="function")throw new TypeError("callback only supported with file option");return e.length&&Ast(s,e),s.noResume||fst(s),s.file&&s.sync?pst(s):s.file?hst(s,r):f0e(s)};var fst=t=>{let e=t.onentry;t.onentry=e?r=>{e(r),r.resume()}:r=>r.resume()},Ast=(t,e)=>{let r=new Map(e.map(n=>[rG(n),!0])),s=t.filter,a=(n,c)=>{let f=c||c0e.parse(n).root||".",p=n===f?!1:r.has(n)?r.get(n):a(c0e.dirname(n),f);return r.set(n,p),p};t.filter=s?(n,c)=>s(n,c)&&a(rG(n)):n=>a(rG(n))},pst=t=>{let e=f0e(t),r=t.file,s=!0,a;try{let n=GI.statSync(r),c=t.maxReadSize||16*1024*1024;if(n.size