dane-data 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ import pandas as pd
2
+ import streamlit as st
3
+
4
+ import dane_data as dd
5
+ from dane_data.app._ui import (fmt_num, MAX_SERIES, args_py, barras, botones_descarga, codigo, fmt_grande, fmt_pct,
6
+ fmt_pesos, grafico, linea, rutas_actuales, seleccion_geih, serie_geih)
7
+
8
+ st.title("👥 Indicadores del mercado laboral")
9
+ sel = seleccion_geih(clave="geih")
10
+
11
+
12
+ def calcular(por, anual):
13
+ """Indicadores año por año, guardados en disco (con los filtros y meses de la barra lateral)."""
14
+ return serie_geih(tuple(sel["anios"]), por, anual, sel["empalme"], sel["filtros"],
15
+ tuple(sel["meses"]) if sel["meses"] else None, sel["meses_disponibles"], rutas_actuales())
16
+
17
+
18
+ total = calcular(None, True)
19
+ if total.empty or not total["n_muestra"].sum():
20
+ st.warning("Ninguna persona cumple los filtros elegidos.")
21
+ st.stop()
22
+ mensual = calcular(None, False)
23
+ meses_ult = int((mensual["anio"] == total["anio"].iloc[-1]).sum())
24
+ st.caption(f"**Datos:** {sel['descripcion']} · {fmt_num(total['n_muestra'].sum())} personas en la muestra")
25
+
26
+ INDICADORES = {
27
+ "Tasa de desocupación (TD)": ("td", True),
28
+ "Tasa global de participación (TGP)": ("tgp", True),
29
+ "Tasa de ocupación (TO)": ("to", True),
30
+ "Tasa de informalidad": ("tasa_informalidad", True),
31
+ "Ingreso laboral mediano": ("ingreso_mediano", False),
32
+ "Ingreso laboral medio": ("ingreso_medio", False),
33
+ "Ocupados (personas)": ("ocupados", False),
34
+ "Desocupados (personas)": ("desocupados", False),
35
+ "Fuerza de trabajo (personas)": ("fuerza_trabajo", False),
36
+ }
37
+ DESAGREGAR = {
38
+ "Total nacional": None,
39
+ "Sexo": "sexo",
40
+ "Zona (urbano/rural)": "zona",
41
+ "Grupo de edad": "grupo_edad",
42
+ "Nivel educativo": "nivel_educativo",
43
+ "Departamento": "departamento",
44
+ }
45
+
46
+ # ---------------------------------------------------------------- KPIs
47
+ ult = total.iloc[-1]
48
+ prev = total.iloc[-2] if len(total) > 1 else None
49
+
50
+
51
+ def delta(col, pct=True):
52
+ if prev is None or pd.isna(prev[col]):
53
+ return None
54
+ d = ult[col] - prev[col]
55
+ return f"{d:+.1f} p.p. vs {int(prev['anio'])}" if pct else f"{100 * d / prev[col]:+.1f} % vs {int(prev['anio'])}"
56
+
57
+
58
+ st.markdown(f"##### Resumen {int(ult['anio'])}")
59
+ k = st.columns(5)
60
+ k[0].metric("Desocupación", fmt_pct(ult.td), delta("td"), delta_color="inverse",
61
+ help=dd.geih.GLOSARIO["TD"])
62
+ k[1].metric("Participación (TGP)", fmt_pct(ult.tgp), delta("tgp"), help=dd.geih.GLOSARIO["TGP"])
63
+ k[2].metric("Ocupación (TO)", fmt_pct(ult.to), delta("to"), help=dd.geih.GLOSARIO["TO"])
64
+ k[3].metric("Informalidad", fmt_pct(ult.tasa_informalidad), delta("tasa_informalidad"), delta_color="inverse",
65
+ help=dd.geih.GLOSARIO["Informalidad"])
66
+ k[4].metric("Ingreso laboral mediano", fmt_pesos(ult.ingreso_mediano), delta("ingreso_mediano", pct=False),
67
+ help=dd.geih.GLOSARIO["Ingreso laboral"] + " Pesos corrientes (sin ajustar por inflación).")
68
+ if meses_ult < 12:
69
+ st.info(f"ℹ️ {int(ult['anio'])} tiene solo {meses_ult} mes(es) en la selección. La comparación con el año "
70
+ "anterior mezcla temporadas distintas (el desempleo es estacional); para comparar bien elija los "
71
+ "mismos meses en ambos años (desactive «Todos los meses»).")
72
+ st.caption(f"Ocupados: **{fmt_grande(ult.ocupados)}** · Desocupados: **{fmt_grande(ult.desocupados)}** · "
73
+ f"Población en edad de trabajar: **{fmt_grande(ult.pet)}** (promedio mensual).")
74
+
75
+ st.divider()
76
+
77
+ # ---------------------------------------------------------------- Gráfico
78
+ c1, c2, c3 = st.columns([2, 2, 1])
79
+ nombre_ind = c1.selectbox("Indicador", list(INDICADORES))
80
+ nombre_por = c2.selectbox("Desagregar por", list(DESAGREGAR))
81
+ frecuencia = c3.radio("Frecuencia", ["Anual", "Mensual"], horizontal=True)
82
+ col, es_pct = INDICADORES[nombre_ind]
83
+ por = DESAGREGAR[nombre_por]
84
+ anual = frecuencia == "Anual"
85
+
86
+ res = calcular(por, anual)
87
+ if por:
88
+ res = res[res[por].notna()]
89
+ if por: # grupos sin dato (p. ej. 0–14 años no está en la PET)
90
+ res = res[res.groupby(por, observed=True)[col].transform("count") > 0]
91
+ if not anual:
92
+ res["periodo"] = pd.to_datetime(dict(year=res["anio"], month=res["mes"], day=1))
93
+ x = "anio" if anual else "periodo"
94
+
95
+ grupos = list(res[por].dropna().unique()) if por else []
96
+ if por and len(grupos) > MAX_SERIES and res[x].nunique() > 1:
97
+ orden = res.groupby(por, observed=True)["ocupados"].sum().sort_values(ascending=False).index.tolist()
98
+ elegidos = st.multiselect(f"Hay {len(grupos)} grupos; elija hasta {MAX_SERIES} para comparar en el tiempo",
99
+ orden, default=orden[:5], max_selections=MAX_SERIES)
100
+ res_graf = res[res[por].isin(elegidos)]
101
+ else:
102
+ res_graf = res
103
+
104
+ titulo = nombre_ind + (" (%)" if es_pct else "")
105
+ if res_graf[x].nunique() > 1:
106
+ fig = linea(res_graf, x=x, y=col, color=por, titulo_y=titulo, pct=es_pct)
107
+ else:
108
+ fig = barras(res_graf, x=por or x, y=col, titulo_y=titulo, pct=es_pct,
109
+ horizontal=bool(por) and len(grupos) > 6)
110
+ grafico(fig)
111
+
112
+ pocos = res[res["n_muestra"] < 100]
113
+ if len(pocos):
114
+ st.warning(f"⚠️ {len(pocos)} celda(s) tienen menos de 100 personas en la muestra: esas cifras son "
115
+ "poco confiables (mucho error de muestreo). Considere agrupar más meses o menos categorías.")
116
+
117
+ # ---------------------------------------------------------------- Tabla
118
+ with st.expander("📋 Tabla completa", expanded=False):
119
+ tabla = res.drop(columns=["periodo"], errors="ignore")
120
+ st.dataframe(
121
+ tabla, hide_index=True, width="stretch",
122
+ column_config={
123
+ "anio": st.column_config.NumberColumn("Año", format="%d"),
124
+ "mes": st.column_config.NumberColumn("Mes", format="%d"),
125
+ **{c: st.column_config.NumberColumn(c.upper(), format="%.1f %%") for c in ("tgp", "to", "td")},
126
+ "tasa_informalidad": st.column_config.NumberColumn("Informalidad", format="%.1f %%"),
127
+ **{c: st.column_config.NumberColumn(c.replace("_", " ").capitalize(), format="localized")
128
+ for c in ("pet", "fuerza_trabajo", "ocupados", "desocupados", "inactivos", "ingreso_medio",
129
+ "ingreso_mediano", "n_muestra")},
130
+ },
131
+ )
132
+ botones_descarga(tabla, f"indicadores_geih_{'_'.join(map(str, sel['anios']))}", "ind")
133
+
134
+ with st.expander("📖 ¿Qué significa cada indicador?"):
135
+ for k_, v in dd.geih.GLOSARIO.items():
136
+ st.markdown(f"**{k_}.** {v}")
137
+ st.markdown(
138
+ "**Promedio mensual.** La GEIH es mensual: al juntar varios meses, los niveles (personas) son el "
139
+ "promedio de los meses y las tasas se calculan sobre esos promedios, como hace el DANE.\n\n"
140
+ "**Muestra vs población.** `n_muestra` es cuántas personas se encuestaron; los niveles usan el "
141
+ "factor de expansión para representar a todo el país."
142
+ )
143
+
144
+ argumentos = f'{f", por={por!r}" if por else ""}{", anual=False" if not anual else ""}'
145
+ codigo(f'''
146
+ import dane_data as dd
147
+
148
+ # Igual a dd.geih.indicadores(...) sobre los microdatos filtrados, pero año por año y
149
+ # guardado en caché: la segunda vez es instantáneo.
150
+ ind = dd.geih.serie_indicadores({sel["anios"]!r}{argumentos}{args_py(empalme=sel["empalme"], filtros=sel["filtros"], meses=sel["meses"])})
151
+ ind
152
+ ''')
@@ -0,0 +1,79 @@
1
+ import streamlit as st
2
+
3
+ import dane_data as dd
4
+ from dane_data.app._ui import disponibles_eam, disponibles_geih, rutas_actuales
5
+
6
+ st.title("📊 DANE-data")
7
+ st.markdown(
8
+ "#### Microdatos del DANE listos para analizar\n"
9
+ "Esta aplicación descarga, organiza y resume dos encuestas oficiales de Colombia publicadas por el DANE:"
10
+ )
11
+
12
+ c1, c2 = st.columns(2)
13
+ with c1, st.container(border=True):
14
+ st.markdown(
15
+ "##### 👥 GEIH — Gran Encuesta Integrada de Hogares\n"
16
+ "Encuesta **mensual** a ~70.000 personas. Es la fuente oficial del **desempleo**, la "
17
+ "**informalidad** y los **ingresos laborales**. Cada fila es una persona; el *factor de "
18
+ "expansión* dice a cuántas personas del país representa.\n\n"
19
+ "**Úsela para:** brechas de género, desempleo juvenil, informalidad por región, "
20
+ "ingresos frente al salario mínimo…"
21
+ )
22
+ with c2, st.container(border=True):
23
+ st.markdown(
24
+ "##### 🏭 EAM — Encuesta Anual Manufacturera\n"
25
+ "Censo **anual** de ~7.000 establecimientos industriales (1992–2024). Cada fila es una "
26
+ "planta: producción, valor agregado, empleo, salarios, activos y energía.\n\n"
27
+ "**Úsela para:** productividad por sector, participación laboral, concentración "
28
+ "regional, funciones de producción y costos…"
29
+ )
30
+
31
+ st.subheader("¿Cómo empiezo?")
32
+ st.markdown(
33
+ "1. **Datos:** si aún no los tiene, vaya a **⬇️ Descargar datos** (menú izquierdo). "
34
+ "Si ya los descargó, indique la carpeta abajo.\n"
35
+ "2. **Explore:** abra **👥 Indicadores laborales** o **🏭 Indicadores industriales**, elija años y filtros.\n"
36
+ "3. **Exporte:** cada tabla se descarga en CSV, Excel o Stata. Cada página muestra el "
37
+ "**código Python** equivalente para pasar a un notebook cuando esté listo."
38
+ )
39
+
40
+ # ---------------------------------------------------------------- Estado de datos
41
+ st.subheader("Datos disponibles en este computador")
42
+ rutas = rutas_actuales()
43
+ g, e, ge = disponibles_geih(False, rutas), disponibles_eam(rutas), disponibles_geih(True, rutas)
44
+
45
+ m1, m2, m3 = st.columns(3)
46
+ m1.metric("GEIH (serie principal)", f"{g.anio.nunique()} años" if len(g) else "Sin datos",
47
+ f"{g.anio.min()}–{g.anio.max()} · {len(g)} meses" if len(g) else None, delta_color="off")
48
+ m2.metric("GEIH (serie de empalme)", f"{ge.anio.nunique()} años" if len(ge) else "Sin datos",
49
+ f"{ge.anio.min()}–{ge.anio.max()}" if len(ge) else None, delta_color="off")
50
+ m3.metric("EAM", f"{e.anio.nunique()} años" if len(e) else "Sin datos",
51
+ f"{e.anio.min()}–{e.anio.max()}" if len(e) else None, delta_color="off")
52
+
53
+ incompletos = g[~g.completo] if len(g) else g
54
+ if len(incompletos):
55
+ with st.expander(f"⚠️ {len(incompletos)} mes(es) de la GEIH están incompletos y se omitirán"):
56
+ st.dataframe(incompletos[["anio", "nombre_mes", "carpeta", "faltantes"]], hide_index=True)
57
+ st.caption("Vuelva a descargarlos en ⬇️ Descargar datos (borre antes la carpeta de ese mes).")
58
+
59
+ # ---------------------------------------------------------------- Configuración
60
+ with st.expander("⚙️ Carpetas de datos", expanded=not (len(g) or len(e))):
61
+ st.caption(
62
+ "Indique dónde están (o dónde quiere guardar) los datos. Para la GEIH, la carpeta que "
63
+ "contiene `datos_csv/`; para la EAM, la que contiene `datos/`. Se recuerda para la próxima vez."
64
+ )
65
+ with st.form("rutas"):
66
+ p_geih = st.text_input("GEIH serie principal", str(dd.get_path("geih")))
67
+ p_emp = st.text_input("GEIH serie de empalme", str(dd.get_path("geih_empalme")))
68
+ p_eam = st.text_input("EAM", str(dd.get_path("eam")))
69
+ if st.form_submit_button("Guardar", type="primary"):
70
+ dd.set_paths(geih=p_geih, geih_empalme=p_emp, eam=p_eam, save=True)
71
+ st.cache_data.clear()
72
+ st.cache_resource.clear()
73
+ st.rerun()
74
+
75
+ st.divider()
76
+ st.caption(
77
+ "Fuente: DANE — Archivo Nacional de Datos (ANDA), microdatos anonimizados de uso público. "
78
+ "Cite: *DANE, Gran Encuesta Integrada de Hogares (GEIH)* / *Encuesta Anual Manufacturera (EAM)*."
79
+ )
dane_data/cli.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ Línea de comandos de dane_data.
3
+
4
+ dane-data app # abre la aplicación gráfica
5
+ dane-data estado # qué datos hay descargados
6
+ dane-data config --geih RUTA --eam RUTA # dónde están/estarán los datos
7
+ dane-data descargar geih --anios 2024 2025
8
+ dane-data descargar geih --anios 2016 --empalme
9
+ dane-data descargar eam --anios 2022 2023 2024
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import logging
16
+ import socket
17
+ import subprocess
18
+ import sys
19
+ import threading
20
+ import time
21
+ import urllib.request
22
+ import webbrowser
23
+ from pathlib import Path
24
+
25
+ from . import config, eam, geih
26
+
27
+
28
+ def _puerto_libre(preferido: int) -> int:
29
+ """El puerto pedido si está libre; si no, el siguiente libre."""
30
+ for puerto in range(preferido, preferido + 50):
31
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
32
+ if s.connect_ex(("127.0.0.1", puerto)) != 0:
33
+ return puerto
34
+ return preferido
35
+
36
+
37
+ def _abrir_navegador_cuando_lista(url: str, proceso: subprocess.Popen, espera_max: float = 120) -> None:
38
+ """Espera a que Streamlit responda y abre el navegador (una sola vez)."""
39
+ salud = url.rstrip("/") + "/_stcore/health"
40
+ limite = time.time() + espera_max
41
+ while time.time() < limite and proceso.poll() is None:
42
+ try:
43
+ with urllib.request.urlopen(salud, timeout=1) as r:
44
+ if r.status == 200:
45
+ webbrowser.open(url)
46
+ return
47
+ except OSError:
48
+ pass
49
+ time.sleep(0.5)
50
+
51
+
52
+ def _cmd_app(args) -> int:
53
+ app = Path(__file__).parent / "app" / "Inicio.py"
54
+ puerto = _puerto_libre(args.port)
55
+ url = f"http://localhost:{puerto}"
56
+ # headless: Streamlit no abre el navegador ni pide correo la primera vez; lo abrimos nosotros.
57
+ cmd = [sys.executable, "-m", "streamlit", "run", str(app), "--server.port", str(puerto),
58
+ "--server.address", args.address, "--server.headless", "true",
59
+ "--browser.gatherUsageStats", "false"]
60
+ print(f"\n dane-data se está abriendo en {url}")
61
+ print(" Para cerrarla, cierre esta ventana o presione Ctrl+C.\n", flush=True)
62
+ proceso = subprocess.Popen(cmd, cwd=app.parent) # cwd: para que lea app/.streamlit/config.toml
63
+ if not args.no_browser:
64
+ threading.Thread(target=_abrir_navegador_cuando_lista, args=(url, proceso), daemon=True).start()
65
+ try:
66
+ return proceso.wait()
67
+ except KeyboardInterrupt:
68
+ proceso.terminate()
69
+ try:
70
+ proceso.wait(timeout=10)
71
+ except subprocess.TimeoutExpired:
72
+ proceso.kill()
73
+ return 0
74
+
75
+
76
+ def _cmd_estado(args) -> int:
77
+ for key, label in (("geih", "GEIH principal"), ("geih_empalme", "GEIH empalme"), ("eam", "EAM")):
78
+ print(f"{label:15s} {config.get_path(key)}")
79
+ for emp in (False, True):
80
+ av = geih.available(empalme=emp)
81
+ nombre = "GEIH empalme" if emp else "GEIH principal"
82
+ if av.empty:
83
+ print(f"\n{nombre}: sin datos")
84
+ continue
85
+ resumen = av.groupby("anio").agg(meses=("mes", "size"), incompletos=("completo", lambda s: (~s).sum()))
86
+ print(f"\n{nombre}:")
87
+ print(resumen.to_string())
88
+ av = eam.available()
89
+ print("\nEAM:", "sin datos" if av.empty else f"{len(av)} años ({av.anio.min()}–{av.anio.max()})")
90
+ return 0
91
+
92
+
93
+ def _cmd_config(args) -> int:
94
+ config.set_paths(geih=args.geih, geih_empalme=args.empalme, eam=args.eam, save=True)
95
+ print(f"Guardado en {config.CONFIG_FILE}")
96
+ for k, v in config.show_paths().items():
97
+ print(f" {k:13s} {v}")
98
+ return 0
99
+
100
+
101
+ def _cmd_descargar(args) -> int:
102
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
103
+ if args.encuesta == "geih":
104
+ geih.download(years=args.anios, modules=args.modulos, empalme=args.empalme,
105
+ delete_zips=args.borrar_zips, limit_files=args.limite)
106
+ else:
107
+ eam.download(years=args.anios, include_tic=args.tic, delete_zips=args.borrar_zips)
108
+ return 0
109
+
110
+
111
+ def main(argv=None) -> int:
112
+ p = argparse.ArgumentParser(prog="dane-data", description="Microdatos DANE (GEIH y EAM) para economistas.")
113
+ sub = p.add_subparsers(dest="cmd", required=True)
114
+
115
+ a = sub.add_parser("app", help="Abrir la aplicación gráfica en el navegador")
116
+ a.add_argument("--port", type=int, default=8501, help="Puerto preferido (si está ocupado se usa el siguiente)")
117
+ a.add_argument("--no-browser", action="store_true", help="No abrir el navegador automáticamente")
118
+ a.add_argument("--address", default="localhost", help="Use 0.0.0.0 para compartir en la red local (aula)")
119
+ a.set_defaults(fn=_cmd_app)
120
+
121
+ e = sub.add_parser("estado", help="Mostrar rutas y datos disponibles")
122
+ e.set_defaults(fn=_cmd_estado)
123
+
124
+ c = sub.add_parser("config", help="Configurar carpetas de datos (se guarda para siempre)")
125
+ c.add_argument("--geih", help="Carpeta de la GEIH principal (la que contiene datos_csv/)")
126
+ c.add_argument("--empalme", help="Carpeta de la GEIH empalme (la que contiene datos_csv/)")
127
+ c.add_argument("--eam", help="Carpeta de la EAM (la que contiene datos/)")
128
+ c.set_defaults(fn=_cmd_config)
129
+
130
+ d = sub.add_parser("descargar", help="Descargar microdatos del DANE")
131
+ d.add_argument("encuesta", choices=["geih", "eam"])
132
+ d.add_argument("--anios", nargs="+", type=int, help="Años (por defecto todos)")
133
+ d.add_argument("--modulos", nargs="+", choices=list(geih.MODULE_PATTERNS), help="Solo GEIH")
134
+ d.add_argument("--empalme", action="store_true", help="GEIH: serie de empalme 2010–2020")
135
+ d.add_argument("--limite", type=int, help="GEIH: máximo de archivos mensuales por año (pruebas)")
136
+ d.add_argument("--tic", action="store_true", help="EAM: incluir módulo TIC 2008–2018")
137
+ d.add_argument("--borrar-zips", action="store_true", help="Borrar los ZIP tras extraer")
138
+ d.set_defaults(fn=_cmd_descargar)
139
+
140
+ args = p.parse_args(argv)
141
+ try:
142
+ return args.fn(args)
143
+ except KeyboardInterrupt:
144
+ print("\nInterrumpido.")
145
+ return 130
146
+
147
+
148
+ if __name__ == "__main__":
149
+ sys.exit(main())
dane_data/config.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ Configuración de rutas de datos.
3
+
4
+ Cada encuesta vive en su propia carpeta raíz, con la misma estructura que
5
+ generan los descargadores originales:
6
+
7
+ GEIH (serie principal) : <raiz>/datos_csv/<año>/<mes>/*.csv
8
+ GEIH (serie empalme) : <raiz>/datos_csv/<año>/<mes>/*.csv
9
+ EAM : <raiz>/datos/<año>/*.csv|*.txt
10
+
11
+ Prioridad para resolver cada ruta:
12
+ 1. ``set_paths(...)`` en la sesión actual.
13
+ 2. Variables de entorno ``DANE_DATA_GEIH``, ``DANE_DATA_GEIH_EMPALME``, ``DANE_DATA_EAM``.
14
+ 3. Archivo ``~/.config/dane_data/config.json`` (lo escribe ``save()`` y la app).
15
+ 4. Valor por defecto bajo ``~/dane_data_datos/``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ from pathlib import Path
23
+
24
+ CONFIG_FILE = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "dane_data" / "config.json"
25
+ CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "dane_data"
26
+ DEFAULT_HOME = Path.home() / "dane_data_datos"
27
+
28
+ _KEYS = {
29
+ "geih": ("DANE_DATA_GEIH", DEFAULT_HOME / "geih"),
30
+ "geih_empalme": ("DANE_DATA_GEIH_EMPALME", DEFAULT_HOME / "geih" / "empalme"),
31
+ "eam": ("DANE_DATA_EAM", DEFAULT_HOME / "eam"),
32
+ }
33
+ _session: dict[str, Path] = {}
34
+
35
+
36
+ def _read_file() -> dict:
37
+ try:
38
+ return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
39
+ except (OSError, ValueError):
40
+ return {}
41
+
42
+
43
+ def get_path(key: str) -> Path:
44
+ """Devuelve la carpeta raíz de ``key`` ('geih', 'geih_empalme' o 'eam')."""
45
+ if key not in _KEYS:
46
+ raise KeyError(f"Clave desconocida {key!r}. Opciones: {list(_KEYS)}")
47
+ if key in _session:
48
+ return _session[key]
49
+ env, default = _KEYS[key]
50
+ if os.environ.get(env):
51
+ return Path(os.environ[env]).expanduser()
52
+ stored = _read_file().get(key)
53
+ if stored:
54
+ return Path(stored).expanduser()
55
+ return default
56
+
57
+
58
+ def set_paths(geih=None, geih_empalme=None, eam=None, save: bool = False) -> None:
59
+ """
60
+ Fija las carpetas raíz para esta sesión.
61
+
62
+ >>> import dane_data as dd
63
+ >>> dd.set_paths(geih="~/datos/GEIH/data", eam="~/datos/EAM/data", save=True)
64
+
65
+ Con ``save=True`` se guardan en ``~/.config/dane_data/config.json`` para
66
+ futuras sesiones (y para la app).
67
+ """
68
+ for key, val in (("geih", geih), ("geih_empalme", geih_empalme), ("eam", eam)):
69
+ if val is not None:
70
+ _session[key] = Path(val).expanduser()
71
+ if save:
72
+ data = _read_file()
73
+ data.update({k: str(v) for k, v in _session.items()})
74
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
75
+ CONFIG_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
76
+
77
+
78
+ def show_paths() -> dict[str, str]:
79
+ """Rutas efectivas actuales (útil para depurar)."""
80
+ return {k: str(get_path(k)) for k in _KEYS}
File without changes