habilmente 0.1.0__tar.gz

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,39 @@
1
+ # Entornos y caches de Python
2
+ .venv/
3
+ venv/
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ .coverage.*
11
+ htmlcov/
12
+ coverage.xml
13
+
14
+ # Empaquetado
15
+ build/
16
+ dist/
17
+ *.egg-info/
18
+
19
+ # Secretos y configuración local: nunca se versionan
20
+ .env
21
+ .env.*
22
+ !.env.example
23
+ *.pem
24
+ *.key
25
+ .pypirc
26
+
27
+ # Editores y sistema operativo
28
+ .vscode/
29
+ .idea/
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # Datos locales (este repo solo usa datos sintéticos)
34
+ *.csv
35
+ *.xlsx
36
+ *.parquet
37
+
38
+ # Cachés de pruebas
39
+ .hypothesis/
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ Todos los cambios importantes de este proyecto se documentan aquí. El formato sigue
4
+ [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y el proyecto usa
5
+ [versionado semántico](https://semver.org/lang/es/).
6
+
7
+ ## [Sin publicar]
8
+
9
+ ## [0.1.0] - 2026-11-12
10
+
11
+ Primera versión.
12
+
13
+ ### Agregado
14
+
15
+ - **Festivos nacionales de Colombia de 1984 a 2199**, calculados desde las reglas legales:
16
+ - fijos;
17
+ - trasladables por la Ley 51 de 1983 (Ley Emiliani);
18
+ - dependientes de la Pascua;
19
+ - el 9 de julio desde 2026 (Ley 2578 de 2026).
20
+ - **Funciones de festivos:** `festivos`, `fechas_festivas`, `es_festivo`,
21
+ `nombre_festivo` y `domingo_de_pascua`.
22
+ - **`Calendario` configurable e inmutable:** sábados hábiles por posición en el mes,
23
+ días no hábiles extra y días hábiles extra.
24
+ - **Funciones de días hábiles:** `es_dia_habil`, `explicar_dia`, `sumar_dias_habiles`,
25
+ `siguiente_dia_habil`, `anterior_dia_habil`, `dias_habiles_entre`,
26
+ `listar_dias_habiles`, `dias_habiles_del_mes` y `dia_habil_del_mes`.
27
+ - **Comando `habilmente`:** subcomandos `festivos`, `es-habil`, `sumar`, `siguiente`,
28
+ `anterior`, `entre` y `mes`, con salida en texto, JSON o CSV.
29
+ - **Módulo opcional `habilmente.pandas`:** columnas de días hábiles, festivos, sumas y
30
+ conteos (con soporte de `NaT`, zonas horarias y `errores="coerce"`), más
31
+ `tabla_calendario` para Power BI.
32
+ - **Pruebas:** verificación contra calendarios publicados y dos implementaciones
33
+ independientes, pruebas con hypothesis y fuzzing de seguridad.
34
+ - **CI y seguridad:** GitHub Actions para lint, tipos, tests, seguridad y publicación con
35
+ Trusted Publishing.
36
+
37
+ [Sin publicar]: https://github.com/juanes-15/habilmente/compare/v0.1.0...HEAD
38
+ [0.1.0]: https://github.com/juanes-15/habilmente/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Roncancio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.5
2
+ Name: habilmente
3
+ Version: 0.1.0
4
+ Summary: Festivos de Colombia y cálculo de días hábiles con calendarios laborales configurables.
5
+ Project-URL: Repository, https://github.com/juanes-15/habilmente
6
+ Project-URL: Issues, https://github.com/juanes-15/habilmente/issues
7
+ Project-URL: Changelog, https://github.com/juanes-15/habilmente/blob/main/CHANGELOG.md
8
+ Project-URL: Security, https://github.com/juanes-15/habilmente/security/policy
9
+ Author: Juan Roncancio
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: calendario laboral,colombia,dias habiles,festivos,holidays,ley emiliani
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: Spanish
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Office/Business :: Scheduling
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Provides-Extra: dev
26
+ Requires-Dist: bandit>=1.8; extra == 'dev'
27
+ Requires-Dist: holidays>=0.100; extra == 'dev'
28
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
29
+ Requires-Dist: mypy>=1.13; extra == 'dev'
30
+ Requires-Dist: pandas-stubs; extra == 'dev'
31
+ Requires-Dist: pandas>=2.0; extra == 'dev'
32
+ Requires-Dist: pip-audit>=2.7; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
34
+ Requires-Dist: pytest>=8; extra == 'dev'
35
+ Requires-Dist: python-dateutil>=2.9; extra == 'dev'
36
+ Requires-Dist: ruff>=0.8; extra == 'dev'
37
+ Requires-Dist: types-python-dateutil; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # habilmente
41
+
42
+ [![CI](https://github.com/juanes-15/habilmente/actions/workflows/ci.yml/badge.svg)](https://github.com/juanes-15/habilmente/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/habilmente)](https://pypi.org/project/habilmente/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/habilmente)](https://pypi.org/project/habilmente/)
45
+ [![Licencia: MIT](https://img.shields.io/badge/licencia-MIT-blue)](LICENSE)
46
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/juanes-15/habilmente/badge)](https://scorecard.dev/viewer/?uri=github.com/juanes-15/habilmente)
47
+
48
+ **Festivos de Colombia y días hábiles con calendarios laborales configurables.**
49
+ Sin dependencias, verificada contra fuentes externas y lista para pandas y Power BI.
50
+
51
+ ```bash
52
+ pip install habilmente
53
+ ```
54
+
55
+ ## ¿Qué problema resuelve?
56
+
57
+ Saber qué días son festivos es la parte fácil. Lo que se necesita a diario es otra cosa:
58
+ ¿cuándo vence un plazo de 15 días hábiles?, ¿cuántos días hábiles tiene el mes para
59
+ repartir la meta?, ¿cuál es el quinto día hábil para el cierre contable?, ¿cuántos días
60
+ hábiles tardó cada respuesta? Y la respuesta cambia según la empresa: hay quienes
61
+ trabajan el primer y el último sábado del mes, cierran el 24 de diciembre o abren un
62
+ festivo.
63
+
64
+ habilmente calcula todo eso para cualquier año entre 1984 y 2199 aplicando las leyes,
65
+ sin tablas de fechas copiadas a mano:
66
+
67
+ - **Ley 51 de 1983 (Ley Emiliani):** festivos que se trasladan al lunes siguiente.
68
+ - **Festivos que dependen de la Pascua**, calculada con el algoritmo de Meeus/Jones/Butcher.
69
+ - **Ley 2578 de 2026:** el 9 de julio (Virgen de Chiquinquirá), festivo desde 2026.
70
+
71
+ ## Uso en 5 líneas
72
+
73
+ ```python
74
+ >>> from datetime import date
75
+ >>> from habilmente import Calendario
76
+ >>> cal = Calendario(sabados=[1, -1]) # Se trabaja el primer y el último sábado del mes.
77
+ >>> cal.sumar_dias_habiles(date(2026, 1, 9), 15) # Plazo de 15 días hábiles.
78
+ datetime.date(2026, 1, 31)
79
+ >>> cal.explicar_dia(date(2026, 1, 12))
80
+ 'No hábil: festivo (Día de los Reyes Magos)'
81
+
82
+ ```
83
+
84
+ ## Funciones principales
85
+
86
+ Todas existen como funciones del módulo (calendario estándar de lunes a viernes) y como
87
+ métodos de un `Calendario` configurado.
88
+
89
+ | Función | Para qué sirve |
90
+ |---|---|
91
+ | `festivos(año)` | Los festivos del año, con nombre, regla legal y fecha original antes del traslado |
92
+ | `es_festivo(fecha)` / `nombre_festivo(fecha)` | Saber si una fecha es festivo y cuál |
93
+ | `es_dia_habil(fecha)` / `explicar_dia(fecha)` | Saber si se trabaja y por qué regla |
94
+ | `sumar_dias_habiles(fecha, n)` | Vencimiento de plazos (acepta `n` negativo) |
95
+ | `siguiente_dia_habil(fecha)` / `anterior_dia_habil(fecha)` | El día hábil más cercano |
96
+ | `dias_habiles_entre(inicio, fin)` | Tiempos de respuesta y SLA |
97
+ | `listar_dias_habiles(inicio, fin)` | Los días hábiles de un rango |
98
+ | `dias_habiles_del_mes(año, mes)` | Para repartir metas mensuales |
99
+ | `dia_habil_del_mes(año, mes, n)` | Fechas de corte: el quinto día hábil, el último (`-1`) |
100
+
101
+ ```python
102
+ >>> import habilmente as h
103
+ >>> h.nombre_festivo(date(2025, 6, 30)) # Dos celebraciones el mismo día.
104
+ 'Sagrado Corazón de Jesús / San Pedro y San Pablo'
105
+ >>> h.dia_habil_del_mes(2026, 12, -1) # Último día hábil del año.
106
+ datetime.date(2026, 12, 31)
107
+ >>> len(h.dias_habiles_del_mes(2026, 11))
108
+ 19
109
+
110
+ ```
111
+
112
+ ### Calendario configurable
113
+
114
+ ```python
115
+ >>> cal = Calendario(
116
+ ... sabados=[1, -1], # None (ninguno), "todos" o posiciones: 1..5 y -1..-5
117
+ ... dias_no_habiles_extra=[date(2026, 12, 24)], # Cierres propios de la empresa.
118
+ ... dias_habiles_extra=[date(2026, 12, 8)], # Festivos que sí se trabajan.
119
+ ... )
120
+ >>> cal.es_dia_habil(date(2026, 12, 8))
121
+ True
122
+
123
+ ```
124
+
125
+ Cuando las reglas chocan, gana la de mayor prioridad: `dias_habiles_extra` →
126
+ `dias_no_habiles_extra` → festivo → sábados configurados → lunes a viernes. Si un sábado
127
+ configurado cae en festivo, no se trabaja y la regla no se corre al sábado siguiente.
128
+
129
+ ## Comando de terminal
130
+
131
+ ```bash
132
+ habilmente 2027 # festivos del año (sin nada: el año actual)
133
+ habilmente 2027 --formato csv > festivos.csv # también json
134
+ habilmente es-habil 2026-11-02 --sabados 1,-1 # código de salida 0 si es hábil, 1 si no
135
+ habilmente sumar 2026-10-30 10
136
+ habilmente entre 2026-10-01 2026-10-31
137
+ habilmente mes 2026 11 --n 5 # quinto día hábil de noviembre
138
+ ```
139
+
140
+ ## pandas y Power BI
141
+
142
+ ```bash
143
+ pip install "habilmente[pandas]"
144
+ ```
145
+
146
+ ```python
147
+ from habilmente.pandas import dias_habiles_entre, marcar_dias_habiles, tabla_calendario
148
+
149
+ df = marcar_dias_habiles(df, "fecha", cal) # copia con columna es_habil
150
+ df["sla"] = dias_habiles_entre(df["radicado"], df["respuesta"]) # días hábiles por fila
151
+ dim_fecha = tabla_calendario(date(2026, 1, 1), date(2026, 12, 31), cal)
152
+ dim_fecha.to_csv("dim_fecha.csv", index=False) # para importar en Power BI
153
+ ```
154
+
155
+ - **Tabla calendario:** trae año, trimestre, mes, día de la semana en español, semana
156
+ ISO, si es festivo y cuál, si es hábil, número de día hábil del mes y si es el último
157
+ día hábil del mes.
158
+ - **Fechas vacías y zonas horarias:** las fechas vacías (`NaT`) dan un resultado vacío,
159
+ sin error. Con zona horaria se usa la fecha local.
160
+ - **Fechas fuera de rango:** con `errores="coerce"` quedan vacías en vez de detener el
161
+ proceso.
162
+ - **Velocidad:** un millón de filas se procesa en centésimas de segundo, porque cada
163
+ fecha distinta se calcula una sola vez.
164
+
165
+ ## ¿Por qué habilmente?
166
+
167
+ | | habilmente | holidays | colombian-holidays / holidays-co |
168
+ |---|:---:|:---:|:---:|
169
+ | Festivos de Colombia | ✅ | ✅ | ✅ |
170
+ | Contar y sumar días hábiles | ✅ | ✅ básico | — |
171
+ | Sábados hábiles por posición en el mes | ✅ | — | — |
172
+ | Días extra hábiles o no hábiles de la empresa | ✅ | — | — |
173
+ | Explicación de por qué un día es o no hábil | ✅ | — | — |
174
+ | N-ésimo y último día hábil del mes | ✅ | — | — |
175
+ | Integración con pandas y tabla para Power BI | ✅ | — | — |
176
+ | Comando de terminal en español | ✅ | — | — |
177
+ | Sin dependencias | ✅ | — | — |
178
+
179
+ Comparación hecha con `holidays` 0.107 y con la descripción publicada en PyPI de
180
+ `colombian-holidays` 1.0.1 y `holidays-co` 1.1.3, en noviembre de 2026.
181
+
182
+ ## Cómo se verificó
183
+
184
+ - **Calendarios publicados:** 2019 a 2027 coinciden día por día. El festivo nuevo de 2026
185
+ se confirmó con el Ministerio del Interior.
186
+ - **Dos implementaciones independientes:** todos los años de 1984 a 2199 se comparan con
187
+ unas reglas escritas aparte en los tests y con la Pascua de `dateutil`; hasta 2100,
188
+ también con la librería `holidays`.
189
+ - **Casos aleatorios (hypothesis):** miles de casos por ejecución comparan la librería
190
+ contra un cálculo ingenuo día por día. También se comprueban propiedades matemáticas:
191
+ sumar y contar son inversos, contar es antisimétrico y aditivo, y "siguiente" nunca
192
+ salta un día hábil.
193
+ - **Calidad:** 100 % de cobertura de líneas y ramas, mypy en modo estricto, y CI en
194
+ Linux, Windows y macOS con Python 3.11 a 3.13 y pandas 2 y 3.
195
+
196
+ ## Seguridad
197
+
198
+ habilmente no usa red, no lee ni escribe archivos y no ejecuta código externo. Aun así se
199
+ prueba como si alguien quisiera romperla:
200
+
201
+ - **Fuzzing de toda la API y del comando de terminal:** solo puede fallar con errores
202
+ controlados (`TypeError`, `ValueError`).
203
+ - **Objetos que intentan engañar la validación:** enteros que mienten en las
204
+ comparaciones o fechas con métodos falsos no logran saltarse los rangos ni inflar la
205
+ memoria.
206
+ - **Denegación de servicio:** números de miles de dígitos, rangos máximos e iterables
207
+ infinitos terminan rápido con un error claro.
208
+ - **Terminal:** los códigos de control en los argumentos se neutralizan antes de mostrar
209
+ cualquier mensaje.
210
+ - **Código fuente:** un test prohíbe `eval`, `exec`, `pickle`, `subprocess`, red y acceso
211
+ a archivos.
212
+ - **Cadena de suministro:**
213
+ - GitHub Actions fijadas a un commit exacto y con permisos mínimos;
214
+ - auditoría de los workflows con zizmor, y análisis con CodeQL, bandit y pip-audit;
215
+ - OpenSSF Scorecard y Dependabot con 7 días de espera antes de adoptar versiones nuevas;
216
+ - publicación en PyPI con Trusted Publishing (sin tokens) y atestaciones de procedencia.
217
+
218
+ Para reportar una vulnerabilidad, revisa [SECURITY.md](SECURITY.md).
219
+
220
+ ## Decisiones técnicas
221
+
222
+ - **Convención de plazos:** `sumar_dias_habiles(f, n)` y `dias_habiles_entre(a, b)` no
223
+ cuentan el día de inicio y sí el final, como un plazo legal. Para listar un rango con
224
+ los dos extremos incluidos está `listar_dias_habiles`.
225
+ - **No se aceptan textos como fecha**, porque `01/02` puede ser enero o febrero. En la
226
+ terminal solo se acepta `AAAA-MM-DD`.
227
+ - **Todo es inmutable:** `Calendario`, `Festivo` y los resultados se pueden compartir
228
+ entre hilos sin riesgo.
229
+ - **Rendimiento:** cada año se calcula una sola vez por calendario y las búsquedas son
230
+ binarias. Sumar 20 días hábiles toma menos de un microsegundo.
231
+ - **Rango 1984–2199:** antes de 1984 regían otras reglas. Los años futuros son una
232
+ proyección con las leyes vigentes; si el Congreso crea o mueve un festivo, se publica
233
+ una versión nueva.
234
+
235
+ ## Roadmap
236
+
237
+ - [ ] Festivos regionales o de entidades específicas (por ejemplo, la Rama Judicial).
238
+ - [ ] Jornadas parciales (medio día hábil).
239
+ - [ ] Integración con polars.
240
+ - [ ] Documentación en un sitio web con ejemplos para analistas.
241
+
242
+ ## Desarrollo
243
+
244
+ ```bash
245
+ git clone https://github.com/juanes-15/habilmente.git
246
+ cd habilmente
247
+ python -m venv .venv && source .venv/bin/activate
248
+ pip install -e ".[dev]"
249
+ pytest --cov # tests, doctests de este README y cobertura
250
+ ruff check . && mypy # lint y tipos
251
+ ```
252
+
253
+ ## Licencia
254
+
255
+ [MIT](LICENSE) © Juan Roncancio
256
+
257
+ ---
258
+
259
+ ## English summary
260
+
261
+ **habilmente** is a dependency-free Python library for Colombian public holidays and
262
+ business-day arithmetic.
263
+
264
+ - **Holiday rules:** it computes holidays for 1984–2199 from the law: the Emiliani Law
265
+ (Monday shifts), Easter-based holidays and Law 2578 of 2026. There are no hard-coded
266
+ date tables.
267
+ - **Configurable work calendars:** specific Saturdays by position in the month (e.g.
268
+ first and last), company closures and worked holidays.
269
+ - **Business-day functions:** add business days, count them between dates, and get the
270
+ n-th or last business day of a month. It also explains why each day is or isn't a
271
+ business day.
272
+ - **Extras:** a Spanish CLI, and pandas helpers including a date dimension table for
273
+ Power BI.
274
+ - **Verification:** results are checked against published calendars and two independent
275
+ implementations, with property-based tests and fuzzing for security.
@@ -0,0 +1,236 @@
1
+ # habilmente
2
+
3
+ [![CI](https://github.com/juanes-15/habilmente/actions/workflows/ci.yml/badge.svg)](https://github.com/juanes-15/habilmente/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/habilmente)](https://pypi.org/project/habilmente/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/habilmente)](https://pypi.org/project/habilmente/)
6
+ [![Licencia: MIT](https://img.shields.io/badge/licencia-MIT-blue)](LICENSE)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/juanes-15/habilmente/badge)](https://scorecard.dev/viewer/?uri=github.com/juanes-15/habilmente)
8
+
9
+ **Festivos de Colombia y días hábiles con calendarios laborales configurables.**
10
+ Sin dependencias, verificada contra fuentes externas y lista para pandas y Power BI.
11
+
12
+ ```bash
13
+ pip install habilmente
14
+ ```
15
+
16
+ ## ¿Qué problema resuelve?
17
+
18
+ Saber qué días son festivos es la parte fácil. Lo que se necesita a diario es otra cosa:
19
+ ¿cuándo vence un plazo de 15 días hábiles?, ¿cuántos días hábiles tiene el mes para
20
+ repartir la meta?, ¿cuál es el quinto día hábil para el cierre contable?, ¿cuántos días
21
+ hábiles tardó cada respuesta? Y la respuesta cambia según la empresa: hay quienes
22
+ trabajan el primer y el último sábado del mes, cierran el 24 de diciembre o abren un
23
+ festivo.
24
+
25
+ habilmente calcula todo eso para cualquier año entre 1984 y 2199 aplicando las leyes,
26
+ sin tablas de fechas copiadas a mano:
27
+
28
+ - **Ley 51 de 1983 (Ley Emiliani):** festivos que se trasladan al lunes siguiente.
29
+ - **Festivos que dependen de la Pascua**, calculada con el algoritmo de Meeus/Jones/Butcher.
30
+ - **Ley 2578 de 2026:** el 9 de julio (Virgen de Chiquinquirá), festivo desde 2026.
31
+
32
+ ## Uso en 5 líneas
33
+
34
+ ```python
35
+ >>> from datetime import date
36
+ >>> from habilmente import Calendario
37
+ >>> cal = Calendario(sabados=[1, -1]) # Se trabaja el primer y el último sábado del mes.
38
+ >>> cal.sumar_dias_habiles(date(2026, 1, 9), 15) # Plazo de 15 días hábiles.
39
+ datetime.date(2026, 1, 31)
40
+ >>> cal.explicar_dia(date(2026, 1, 12))
41
+ 'No hábil: festivo (Día de los Reyes Magos)'
42
+
43
+ ```
44
+
45
+ ## Funciones principales
46
+
47
+ Todas existen como funciones del módulo (calendario estándar de lunes a viernes) y como
48
+ métodos de un `Calendario` configurado.
49
+
50
+ | Función | Para qué sirve |
51
+ |---|---|
52
+ | `festivos(año)` | Los festivos del año, con nombre, regla legal y fecha original antes del traslado |
53
+ | `es_festivo(fecha)` / `nombre_festivo(fecha)` | Saber si una fecha es festivo y cuál |
54
+ | `es_dia_habil(fecha)` / `explicar_dia(fecha)` | Saber si se trabaja y por qué regla |
55
+ | `sumar_dias_habiles(fecha, n)` | Vencimiento de plazos (acepta `n` negativo) |
56
+ | `siguiente_dia_habil(fecha)` / `anterior_dia_habil(fecha)` | El día hábil más cercano |
57
+ | `dias_habiles_entre(inicio, fin)` | Tiempos de respuesta y SLA |
58
+ | `listar_dias_habiles(inicio, fin)` | Los días hábiles de un rango |
59
+ | `dias_habiles_del_mes(año, mes)` | Para repartir metas mensuales |
60
+ | `dia_habil_del_mes(año, mes, n)` | Fechas de corte: el quinto día hábil, el último (`-1`) |
61
+
62
+ ```python
63
+ >>> import habilmente as h
64
+ >>> h.nombre_festivo(date(2025, 6, 30)) # Dos celebraciones el mismo día.
65
+ 'Sagrado Corazón de Jesús / San Pedro y San Pablo'
66
+ >>> h.dia_habil_del_mes(2026, 12, -1) # Último día hábil del año.
67
+ datetime.date(2026, 12, 31)
68
+ >>> len(h.dias_habiles_del_mes(2026, 11))
69
+ 19
70
+
71
+ ```
72
+
73
+ ### Calendario configurable
74
+
75
+ ```python
76
+ >>> cal = Calendario(
77
+ ... sabados=[1, -1], # None (ninguno), "todos" o posiciones: 1..5 y -1..-5
78
+ ... dias_no_habiles_extra=[date(2026, 12, 24)], # Cierres propios de la empresa.
79
+ ... dias_habiles_extra=[date(2026, 12, 8)], # Festivos que sí se trabajan.
80
+ ... )
81
+ >>> cal.es_dia_habil(date(2026, 12, 8))
82
+ True
83
+
84
+ ```
85
+
86
+ Cuando las reglas chocan, gana la de mayor prioridad: `dias_habiles_extra` →
87
+ `dias_no_habiles_extra` → festivo → sábados configurados → lunes a viernes. Si un sábado
88
+ configurado cae en festivo, no se trabaja y la regla no se corre al sábado siguiente.
89
+
90
+ ## Comando de terminal
91
+
92
+ ```bash
93
+ habilmente 2027 # festivos del año (sin nada: el año actual)
94
+ habilmente 2027 --formato csv > festivos.csv # también json
95
+ habilmente es-habil 2026-11-02 --sabados 1,-1 # código de salida 0 si es hábil, 1 si no
96
+ habilmente sumar 2026-10-30 10
97
+ habilmente entre 2026-10-01 2026-10-31
98
+ habilmente mes 2026 11 --n 5 # quinto día hábil de noviembre
99
+ ```
100
+
101
+ ## pandas y Power BI
102
+
103
+ ```bash
104
+ pip install "habilmente[pandas]"
105
+ ```
106
+
107
+ ```python
108
+ from habilmente.pandas import dias_habiles_entre, marcar_dias_habiles, tabla_calendario
109
+
110
+ df = marcar_dias_habiles(df, "fecha", cal) # copia con columna es_habil
111
+ df["sla"] = dias_habiles_entre(df["radicado"], df["respuesta"]) # días hábiles por fila
112
+ dim_fecha = tabla_calendario(date(2026, 1, 1), date(2026, 12, 31), cal)
113
+ dim_fecha.to_csv("dim_fecha.csv", index=False) # para importar en Power BI
114
+ ```
115
+
116
+ - **Tabla calendario:** trae año, trimestre, mes, día de la semana en español, semana
117
+ ISO, si es festivo y cuál, si es hábil, número de día hábil del mes y si es el último
118
+ día hábil del mes.
119
+ - **Fechas vacías y zonas horarias:** las fechas vacías (`NaT`) dan un resultado vacío,
120
+ sin error. Con zona horaria se usa la fecha local.
121
+ - **Fechas fuera de rango:** con `errores="coerce"` quedan vacías en vez de detener el
122
+ proceso.
123
+ - **Velocidad:** un millón de filas se procesa en centésimas de segundo, porque cada
124
+ fecha distinta se calcula una sola vez.
125
+
126
+ ## ¿Por qué habilmente?
127
+
128
+ | | habilmente | holidays | colombian-holidays / holidays-co |
129
+ |---|:---:|:---:|:---:|
130
+ | Festivos de Colombia | ✅ | ✅ | ✅ |
131
+ | Contar y sumar días hábiles | ✅ | ✅ básico | — |
132
+ | Sábados hábiles por posición en el mes | ✅ | — | — |
133
+ | Días extra hábiles o no hábiles de la empresa | ✅ | — | — |
134
+ | Explicación de por qué un día es o no hábil | ✅ | — | — |
135
+ | N-ésimo y último día hábil del mes | ✅ | — | — |
136
+ | Integración con pandas y tabla para Power BI | ✅ | — | — |
137
+ | Comando de terminal en español | ✅ | — | — |
138
+ | Sin dependencias | ✅ | — | — |
139
+
140
+ Comparación hecha con `holidays` 0.107 y con la descripción publicada en PyPI de
141
+ `colombian-holidays` 1.0.1 y `holidays-co` 1.1.3, en noviembre de 2026.
142
+
143
+ ## Cómo se verificó
144
+
145
+ - **Calendarios publicados:** 2019 a 2027 coinciden día por día. El festivo nuevo de 2026
146
+ se confirmó con el Ministerio del Interior.
147
+ - **Dos implementaciones independientes:** todos los años de 1984 a 2199 se comparan con
148
+ unas reglas escritas aparte en los tests y con la Pascua de `dateutil`; hasta 2100,
149
+ también con la librería `holidays`.
150
+ - **Casos aleatorios (hypothesis):** miles de casos por ejecución comparan la librería
151
+ contra un cálculo ingenuo día por día. También se comprueban propiedades matemáticas:
152
+ sumar y contar son inversos, contar es antisimétrico y aditivo, y "siguiente" nunca
153
+ salta un día hábil.
154
+ - **Calidad:** 100 % de cobertura de líneas y ramas, mypy en modo estricto, y CI en
155
+ Linux, Windows y macOS con Python 3.11 a 3.13 y pandas 2 y 3.
156
+
157
+ ## Seguridad
158
+
159
+ habilmente no usa red, no lee ni escribe archivos y no ejecuta código externo. Aun así se
160
+ prueba como si alguien quisiera romperla:
161
+
162
+ - **Fuzzing de toda la API y del comando de terminal:** solo puede fallar con errores
163
+ controlados (`TypeError`, `ValueError`).
164
+ - **Objetos que intentan engañar la validación:** enteros que mienten en las
165
+ comparaciones o fechas con métodos falsos no logran saltarse los rangos ni inflar la
166
+ memoria.
167
+ - **Denegación de servicio:** números de miles de dígitos, rangos máximos e iterables
168
+ infinitos terminan rápido con un error claro.
169
+ - **Terminal:** los códigos de control en los argumentos se neutralizan antes de mostrar
170
+ cualquier mensaje.
171
+ - **Código fuente:** un test prohíbe `eval`, `exec`, `pickle`, `subprocess`, red y acceso
172
+ a archivos.
173
+ - **Cadena de suministro:**
174
+ - GitHub Actions fijadas a un commit exacto y con permisos mínimos;
175
+ - auditoría de los workflows con zizmor, y análisis con CodeQL, bandit y pip-audit;
176
+ - OpenSSF Scorecard y Dependabot con 7 días de espera antes de adoptar versiones nuevas;
177
+ - publicación en PyPI con Trusted Publishing (sin tokens) y atestaciones de procedencia.
178
+
179
+ Para reportar una vulnerabilidad, revisa [SECURITY.md](SECURITY.md).
180
+
181
+ ## Decisiones técnicas
182
+
183
+ - **Convención de plazos:** `sumar_dias_habiles(f, n)` y `dias_habiles_entre(a, b)` no
184
+ cuentan el día de inicio y sí el final, como un plazo legal. Para listar un rango con
185
+ los dos extremos incluidos está `listar_dias_habiles`.
186
+ - **No se aceptan textos como fecha**, porque `01/02` puede ser enero o febrero. En la
187
+ terminal solo se acepta `AAAA-MM-DD`.
188
+ - **Todo es inmutable:** `Calendario`, `Festivo` y los resultados se pueden compartir
189
+ entre hilos sin riesgo.
190
+ - **Rendimiento:** cada año se calcula una sola vez por calendario y las búsquedas son
191
+ binarias. Sumar 20 días hábiles toma menos de un microsegundo.
192
+ - **Rango 1984–2199:** antes de 1984 regían otras reglas. Los años futuros son una
193
+ proyección con las leyes vigentes; si el Congreso crea o mueve un festivo, se publica
194
+ una versión nueva.
195
+
196
+ ## Roadmap
197
+
198
+ - [ ] Festivos regionales o de entidades específicas (por ejemplo, la Rama Judicial).
199
+ - [ ] Jornadas parciales (medio día hábil).
200
+ - [ ] Integración con polars.
201
+ - [ ] Documentación en un sitio web con ejemplos para analistas.
202
+
203
+ ## Desarrollo
204
+
205
+ ```bash
206
+ git clone https://github.com/juanes-15/habilmente.git
207
+ cd habilmente
208
+ python -m venv .venv && source .venv/bin/activate
209
+ pip install -e ".[dev]"
210
+ pytest --cov # tests, doctests de este README y cobertura
211
+ ruff check . && mypy # lint y tipos
212
+ ```
213
+
214
+ ## Licencia
215
+
216
+ [MIT](LICENSE) © Juan Roncancio
217
+
218
+ ---
219
+
220
+ ## English summary
221
+
222
+ **habilmente** is a dependency-free Python library for Colombian public holidays and
223
+ business-day arithmetic.
224
+
225
+ - **Holiday rules:** it computes holidays for 1984–2199 from the law: the Emiliani Law
226
+ (Monday shifts), Easter-based holidays and Law 2578 of 2026. There are no hard-coded
227
+ date tables.
228
+ - **Configurable work calendars:** specific Saturdays by position in the month (e.g.
229
+ first and last), company closures and worked holidays.
230
+ - **Business-day functions:** add business days, count them between dates, and get the
231
+ n-th or last business day of a month. It also explains why each day is or isn't a
232
+ business day.
233
+ - **Extras:** a Spanish CLI, and pandas helpers including a date dimension table for
234
+ Power BI.
235
+ - **Verification:** results are checked against published calendars and two independent
236
+ implementations, with property-based tests and fuzzing for security.