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.
- habilmente-0.1.0/.gitignore +39 -0
- habilmente-0.1.0/CHANGELOG.md +38 -0
- habilmente-0.1.0/LICENSE +21 -0
- habilmente-0.1.0/PKG-INFO +275 -0
- habilmente-0.1.0/README.md +236 -0
- habilmente-0.1.0/SECURITY.md +79 -0
- habilmente-0.1.0/pyproject.toml +116 -0
- habilmente-0.1.0/requirements/README.md +22 -0
- habilmente-0.1.0/src/habilmente/__init__.py +60 -0
- habilmente-0.1.0/src/habilmente/__main__.py +5 -0
- habilmente-0.1.0/src/habilmente/_textos.py +14 -0
- habilmente-0.1.0/src/habilmente/_validacion.py +102 -0
- habilmente-0.1.0/src/habilmente/calendario.py +517 -0
- habilmente-0.1.0/src/habilmente/cli.py +359 -0
- habilmente-0.1.0/src/habilmente/festivos.py +224 -0
- habilmente-0.1.0/src/habilmente/pandas.py +405 -0
- habilmente-0.1.0/src/habilmente/pascua.py +45 -0
- habilmente-0.1.0/src/habilmente/py.typed +0 -0
- habilmente-0.1.0/tests/__init__.py +0 -0
- habilmente-0.1.0/tests/calendarios_oficiales.py +44 -0
- habilmente-0.1.0/tests/conftest.py +13 -0
- habilmente-0.1.0/tests/oraculo.py +92 -0
- habilmente-0.1.0/tests/test_calendario.py +378 -0
- habilmente-0.1.0/tests/test_cli.py +273 -0
- habilmente-0.1.0/tests/test_festivos.py +192 -0
- habilmente-0.1.0/tests/test_pandas.py +282 -0
- habilmente-0.1.0/tests/test_pascua.py +37 -0
- habilmente-0.1.0/tests/test_propiedades.py +126 -0
- habilmente-0.1.0/tests/test_seguridad.py +422 -0
|
@@ -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
|
habilmente-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://github.com/juanes-15/habilmente/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/habilmente/)
|
|
44
|
+
[](https://pypi.org/project/habilmente/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
[](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
|
+
[](https://github.com/juanes-15/habilmente/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/habilmente/)
|
|
5
|
+
[](https://pypi.org/project/habilmente/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](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.
|