acodo 0.3.0__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.
- acodo/__init__.py +11 -0
- acodo/logger/__init__.py +13 -0
- acodo/logger/logger.py +249 -0
- acodo/py.typed +0 -0
- acodo/utils/__init__.py +263 -0
- acodo/utils/checkers.py +80 -0
- acodo/utils/converters.py +569 -0
- acodo/utils/getters.py +478 -0
- acodo/utils/parsers.py +270 -0
- acodo/utils/types.py +107 -0
- acodo/utils/validators.py +787 -0
- acodo-0.3.0.dist-info/METADATA +94 -0
- acodo-0.3.0.dist-info/RECORD +15 -0
- acodo-0.3.0.dist-info/WHEEL +4 -0
- acodo-0.3.0.dist-info/licenses/LICENSE +21 -0
acodo/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Acodo Framework: utilidades puras en Python, sin dependencias externas.
|
|
2
|
+
|
|
3
|
+
Subpaquetes:
|
|
4
|
+
|
|
5
|
+
- `acodo.utils`: validación, conversión, parseo, formateo y obtención de valores.
|
|
6
|
+
- `acodo.logger`: logger con destino configurable (consola y/o fichero).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
|
|
11
|
+
__version__ = version("acodo")
|
acodo/logger/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Logger de Acodo: salida configurable a consola y fichero con rotación diaria.
|
|
2
|
+
|
|
3
|
+
Uso:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from acodo.logger import Logger
|
|
7
|
+
|
|
8
|
+
log = Logger("miapp")
|
|
9
|
+
log.info("Procesando %(n)s ficheros", n=3)
|
|
10
|
+
```
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .logger import Logger as Logger
|
acodo/logger/logger.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Logger con destino configurable (consola y/o fichero) sobre ``logging``.
|
|
2
|
+
|
|
3
|
+
Ofrece `Logger`, un wrapper que permite decidir en cada mensaje si se muestra
|
|
4
|
+
por consola, se escribe en el fichero de log o ambos. Se configura una sola vez
|
|
5
|
+
por nombre y no toca el logger raíz.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import pprint
|
|
13
|
+
import sys
|
|
14
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from acodo.utils.validators import valid_filename
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_RESET = "\u001b[0m"
|
|
25
|
+
|
|
26
|
+
_LEVEL_COLORS = {
|
|
27
|
+
logging.DEBUG: "\u001b[30;1m",
|
|
28
|
+
logging.INFO: "\u001b[34;1m",
|
|
29
|
+
logging.WARNING: "\u001b[33;1m",
|
|
30
|
+
logging.ERROR: "\u001b[31;1m",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _prettify(value: Any) -> str:
|
|
35
|
+
"""Convierte un valor en una representación legible de varias líneas.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
value: Valor a formatear.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Cadena formateada con ``pprint`` para contenedores o ``str`` para el
|
|
42
|
+
resto.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(value, (dict, list, tuple, set)):
|
|
45
|
+
return pprint.pformat(value, indent=4, width=120)
|
|
46
|
+
return str(value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _DestinationFilter(logging.Filter):
|
|
50
|
+
"""Filtra registros según el destino solicitado (consola o fichero)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, key: str) -> None:
|
|
53
|
+
super().__init__()
|
|
54
|
+
self._key = key
|
|
55
|
+
|
|
56
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
57
|
+
# Un registro sin la marca (emitido directamente con `logging`) se
|
|
58
|
+
# deja pasar a todos los destinos.
|
|
59
|
+
return bool(getattr(record, self._key, True))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _ConsoleFormatter(logging.Formatter):
|
|
63
|
+
"""Antepone `[NIVEL]` al mensaje, con color solo si la salida es un terminal."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *, color: bool) -> None:
|
|
66
|
+
super().__init__("%(message)s")
|
|
67
|
+
self._color = color
|
|
68
|
+
|
|
69
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
70
|
+
tag = f"[{record.levelname}]"
|
|
71
|
+
if self._color and record.levelno in _LEVEL_COLORS:
|
|
72
|
+
tag = f"{_LEVEL_COLORS[record.levelno]}{tag}{_RESET}"
|
|
73
|
+
return f"{tag} {super().format(record)}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Logger:
|
|
77
|
+
"""Wrapper sobre ``logging`` que permite elegir el destino de cada mensaje.
|
|
78
|
+
|
|
79
|
+
Los valores con nombre se interpolan con ``%(nombre)s``; un ``%`` literal en
|
|
80
|
+
``msg`` debe escribirse ``%%`` cuando se pasan valores.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
name: str,
|
|
86
|
+
*,
|
|
87
|
+
file: Path | None = None,
|
|
88
|
+
debug: bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Crea el logger `name`, o reutiliza el ya configurado con ese nombre.
|
|
91
|
+
|
|
92
|
+
La configuración es idempotente por nombre: solo se realiza la primera vez.
|
|
93
|
+
En llamadas posteriores con el mismo nombre se reutiliza el logger ya
|
|
94
|
+
configurado y se ignoran `debug` y `file`. No se modifica el logger raíz.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name: Nombre del logger.
|
|
98
|
+
file: Ruta del fichero de log, con rotación diaria y 7 copias. Si es
|
|
99
|
+
`None`, solo se escribe por consola. Se crean los directorios que
|
|
100
|
+
falten.
|
|
101
|
+
debug: Si `True`, activa el nivel `DEBUG` (por defecto, `INFO`).
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: Si `name` está vacío o `file` no tiene un nombre de
|
|
105
|
+
fichero válido.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
>>> log = Logger("ejemplo")
|
|
109
|
+
>>> log.info("Procesando %(n)s ficheros", n=3)
|
|
110
|
+
[INFO] Procesando 3 ficheros
|
|
111
|
+
"""
|
|
112
|
+
if not name.strip():
|
|
113
|
+
raise ValueError("El nombre del logger no puede estar vacío.")
|
|
114
|
+
if file is not None and not valid_filename(file.name):
|
|
115
|
+
raise ValueError(f"Nombre de fichero de log no válido: {file.name!r}.")
|
|
116
|
+
|
|
117
|
+
self._logger = logging.getLogger(name)
|
|
118
|
+
if not self._logger.handlers:
|
|
119
|
+
self._configure(file=file, debug=debug)
|
|
120
|
+
|
|
121
|
+
def error(
|
|
122
|
+
self,
|
|
123
|
+
msg: str,
|
|
124
|
+
exc_info: bool = False,
|
|
125
|
+
console: bool = True,
|
|
126
|
+
file: bool = True,
|
|
127
|
+
**kwargs: object,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Registra un mensaje de nivel error.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
msg: Mensaje a registrar.
|
|
133
|
+
exc_info: Si `True`, añade la traza de la excepción activa.
|
|
134
|
+
console: Si `True`, se muestra por consola.
|
|
135
|
+
file: Si `True`, se escribe en el fichero de log.
|
|
136
|
+
**kwargs: Valores para interpolar en `msg` con `%(nombre)s`.
|
|
137
|
+
"""
|
|
138
|
+
self._log(logging.ERROR, msg, console, file, kwargs, exc_info=exc_info)
|
|
139
|
+
|
|
140
|
+
def warning(
|
|
141
|
+
self,
|
|
142
|
+
msg: str,
|
|
143
|
+
console: bool = True,
|
|
144
|
+
file: bool = True,
|
|
145
|
+
**kwargs: object,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Registra un mensaje de nivel aviso.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
msg: Mensaje a registrar.
|
|
151
|
+
console: Si `True`, se muestra por consola.
|
|
152
|
+
file: Si `True`, se escribe en el fichero de log.
|
|
153
|
+
**kwargs: Valores para interpolar en `msg` con `%(nombre)s`.
|
|
154
|
+
"""
|
|
155
|
+
self._log(logging.WARNING, msg, console, file, kwargs)
|
|
156
|
+
|
|
157
|
+
def info(
|
|
158
|
+
self,
|
|
159
|
+
msg: str,
|
|
160
|
+
console: bool = True,
|
|
161
|
+
file: bool = True,
|
|
162
|
+
**kwargs: object,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Registra un mensaje de nivel información.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
msg: Mensaje a registrar.
|
|
168
|
+
console: Si `True`, se muestra por consola.
|
|
169
|
+
file: Si `True`, se escribe en el fichero de log.
|
|
170
|
+
**kwargs: Valores para interpolar en `msg` con `%(nombre)s`.
|
|
171
|
+
"""
|
|
172
|
+
self._log(logging.INFO, msg, console, file, kwargs)
|
|
173
|
+
|
|
174
|
+
def debug(
|
|
175
|
+
self,
|
|
176
|
+
msg: str,
|
|
177
|
+
console: bool = True,
|
|
178
|
+
file: bool = True,
|
|
179
|
+
**kwargs: object,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Registra un mensaje de nivel depuración.
|
|
182
|
+
|
|
183
|
+
Los contenedores (`dict`, `list`, `tuple`, `set`) pasados como valores
|
|
184
|
+
se formatean en varias líneas para facilitar su lectura.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
msg: Mensaje a registrar.
|
|
188
|
+
console: Si `True`, se muestra por consola.
|
|
189
|
+
file: Si `True`, se escribe en el fichero de log.
|
|
190
|
+
**kwargs: Valores para interpolar en `msg` con `%(nombre)s`.
|
|
191
|
+
"""
|
|
192
|
+
values = {name: _prettify(value) for name, value in kwargs.items()}
|
|
193
|
+
self._log(logging.DEBUG, msg, console, file, values)
|
|
194
|
+
|
|
195
|
+
def _configure(self, *, file: Path | None, debug: bool) -> None:
|
|
196
|
+
# Nivel, handler de consola y, si se pide, handler de fichero rotativo.
|
|
197
|
+
self._logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
198
|
+
self._logger.propagate = False
|
|
199
|
+
|
|
200
|
+
color = sys.stdout.isatty() and "NO_COLOR" not in os.environ
|
|
201
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
202
|
+
console_handler.setFormatter(_ConsoleFormatter(color=color))
|
|
203
|
+
console_handler.addFilter(_DestinationFilter("to_console"))
|
|
204
|
+
self._logger.addHandler(console_handler)
|
|
205
|
+
|
|
206
|
+
if file is not None:
|
|
207
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
file_handler = TimedRotatingFileHandler(
|
|
209
|
+
filename=file,
|
|
210
|
+
when="midnight",
|
|
211
|
+
backupCount=7,
|
|
212
|
+
encoding="utf-8",
|
|
213
|
+
)
|
|
214
|
+
file_handler.suffix = "%Y-%m-%d"
|
|
215
|
+
file_handler.setFormatter(
|
|
216
|
+
logging.Formatter(
|
|
217
|
+
fmt="%(asctime)s %(levelname)-10s %(message)s",
|
|
218
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
file_handler.addFilter(_DestinationFilter("to_file"))
|
|
222
|
+
self._logger.addHandler(file_handler)
|
|
223
|
+
|
|
224
|
+
def _log(
|
|
225
|
+
self,
|
|
226
|
+
level: int,
|
|
227
|
+
msg: str,
|
|
228
|
+
console: bool,
|
|
229
|
+
file: bool,
|
|
230
|
+
values: Mapping[str, object],
|
|
231
|
+
*,
|
|
232
|
+
exc_info: bool = False,
|
|
233
|
+
) -> None:
|
|
234
|
+
# `logging` interpola de forma perezosa: solo si el nivel está activo.
|
|
235
|
+
# `stacklevel=3` apunta al código que llamó a `info`, `error`...
|
|
236
|
+
self._logger.log(
|
|
237
|
+
level,
|
|
238
|
+
msg,
|
|
239
|
+
*([values] if values else []),
|
|
240
|
+
exc_info=exc_info,
|
|
241
|
+
extra={"to_console": console, "to_file": file},
|
|
242
|
+
stacklevel=3,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
log = Logger(name="test")
|
|
248
|
+
log.info("Información")
|
|
249
|
+
log.error("Errores")
|
acodo/py.typed
ADDED
|
File without changes
|
acodo/utils/__init__.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
r"""Módulo de funciones utilitarias que pueden reutilizarse en cualquier aplicación.
|
|
2
|
+
|
|
3
|
+
Requiere **Python >= 3.13** y no introduce dependencias.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Uso rápido
|
|
7
|
+
----------
|
|
8
|
+
|
|
9
|
+
Forma parte de `acodo` y se importa desde `acodo.utils`:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from acodo.utils import get_hash, has_required_attrs, parse_slug, to_bytes, valid_email
|
|
13
|
+
|
|
14
|
+
get_hash("abc") # ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
|
|
15
|
+
has_required_attrs("texto", ["upper", "lower"]) # True
|
|
16
|
+
parse_slug("¡Hola, Mundo! Ñandú") # hola-mundo-nandu
|
|
17
|
+
to_bytes("1,5 GiB") # 1610612736
|
|
18
|
+
valid_email("email_sin_punto@ejemplo-com") # False
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Tipos
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
`SocialLink` (definida en `acodo.utils.types`) enumera los tipos de enlace de
|
|
26
|
+
red social que reconoce `valid_social_url`.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Convenio de nombrado
|
|
30
|
+
--------------------
|
|
31
|
+
Las funciones se agrupan por categoría, cada categoría empieza por un
|
|
32
|
+
prefijo y dentro de cada una van en orden alfabético.
|
|
33
|
+
|
|
34
|
+
| Prefijo | Returns | Categoría |
|
|
35
|
+
|-----------|---------------|-----------|
|
|
36
|
+
| `get_*` | `Any` | Obtenedores de valores del entorno, credencial, ID, ... |
|
|
37
|
+
| `has_*` | `bool` | Comprobación de presencia de claves, campos o estructura. |
|
|
38
|
+
| `parse_*` | `str` | Formateado a texto legible (importes, fechas, tamaños, ...). |
|
|
39
|
+
| `to_*` | `Any \| None` | Conversión de tipo o `None` si no puede. |
|
|
40
|
+
| `valid_*` | `bool` | Validación de formato o de dominio de un valor. |
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Errores lanzados
|
|
44
|
+
----------------
|
|
45
|
+
|
|
46
|
+
Solo se lanza `ValueError` ante argumentos ambiguos o inválidos:
|
|
47
|
+
|
|
48
|
+
- `get_*` (algoritmo, rango, longitud...)
|
|
49
|
+
- `parse_*` (`lang`),
|
|
50
|
+
- `valid_ip` (`version`)
|
|
51
|
+
- `valid_password`.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Enlaces de interés
|
|
55
|
+
------------------
|
|
56
|
+
- [Documentación de Python](https://docs.python.org/3/)
|
|
57
|
+
- [PEP 8: guía de estilo](https://peps.python.org/pep-0008/)
|
|
58
|
+
- [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)
|
|
59
|
+
(estilo de los docstrings)
|
|
60
|
+
- Herramientas de Astral: [uv](https://docs.astral.sh/uv/),
|
|
61
|
+
[Ruff](https://docs.astral.sh/ruff/) y [ty](https://docs.astral.sh/ty/)
|
|
62
|
+
- [pytest](https://docs.pytest.org/)
|
|
63
|
+
""" # noqa: E501
|
|
64
|
+
|
|
65
|
+
from .checkers import (
|
|
66
|
+
has_required_attrs as has_required_attrs,
|
|
67
|
+
)
|
|
68
|
+
from .checkers import (
|
|
69
|
+
has_required_fields as has_required_fields,
|
|
70
|
+
)
|
|
71
|
+
from .checkers import (
|
|
72
|
+
has_required_keys as has_required_keys,
|
|
73
|
+
)
|
|
74
|
+
from .converters import (
|
|
75
|
+
to_bool as to_bool,
|
|
76
|
+
)
|
|
77
|
+
from .converters import (
|
|
78
|
+
to_bytes as to_bytes,
|
|
79
|
+
)
|
|
80
|
+
from .converters import (
|
|
81
|
+
to_date as to_date,
|
|
82
|
+
)
|
|
83
|
+
from .converters import (
|
|
84
|
+
to_datetime as to_datetime,
|
|
85
|
+
)
|
|
86
|
+
from .converters import (
|
|
87
|
+
to_decimal as to_decimal,
|
|
88
|
+
)
|
|
89
|
+
from .converters import (
|
|
90
|
+
to_dict as to_dict,
|
|
91
|
+
)
|
|
92
|
+
from .converters import (
|
|
93
|
+
to_float as to_float,
|
|
94
|
+
)
|
|
95
|
+
from .converters import (
|
|
96
|
+
to_int as to_int,
|
|
97
|
+
)
|
|
98
|
+
from .converters import (
|
|
99
|
+
to_json as to_json,
|
|
100
|
+
)
|
|
101
|
+
from .converters import (
|
|
102
|
+
to_list as to_list,
|
|
103
|
+
)
|
|
104
|
+
from .converters import (
|
|
105
|
+
to_path as to_path,
|
|
106
|
+
)
|
|
107
|
+
from .converters import (
|
|
108
|
+
to_port as to_port,
|
|
109
|
+
)
|
|
110
|
+
from .converters import (
|
|
111
|
+
to_str as to_str,
|
|
112
|
+
)
|
|
113
|
+
from .converters import (
|
|
114
|
+
to_timedelta as to_timedelta,
|
|
115
|
+
)
|
|
116
|
+
from .getters import (
|
|
117
|
+
get_config_dir as get_config_dir,
|
|
118
|
+
)
|
|
119
|
+
from .getters import (
|
|
120
|
+
get_env as get_env,
|
|
121
|
+
)
|
|
122
|
+
from .getters import (
|
|
123
|
+
get_file_hash as get_file_hash,
|
|
124
|
+
)
|
|
125
|
+
from .getters import (
|
|
126
|
+
get_hash as get_hash,
|
|
127
|
+
)
|
|
128
|
+
from .getters import (
|
|
129
|
+
get_hmac as get_hmac,
|
|
130
|
+
)
|
|
131
|
+
from .getters import (
|
|
132
|
+
get_now as get_now,
|
|
133
|
+
)
|
|
134
|
+
from .getters import (
|
|
135
|
+
get_otp as get_otp,
|
|
136
|
+
)
|
|
137
|
+
from .getters import (
|
|
138
|
+
get_password_hash as get_password_hash,
|
|
139
|
+
)
|
|
140
|
+
from .getters import (
|
|
141
|
+
get_random_choice as get_random_choice,
|
|
142
|
+
)
|
|
143
|
+
from .getters import (
|
|
144
|
+
get_random_int as get_random_int,
|
|
145
|
+
)
|
|
146
|
+
from .getters import (
|
|
147
|
+
get_random_password as get_random_password,
|
|
148
|
+
)
|
|
149
|
+
from .getters import (
|
|
150
|
+
get_random_token as get_random_token,
|
|
151
|
+
)
|
|
152
|
+
from .getters import (
|
|
153
|
+
get_uuid as get_uuid,
|
|
154
|
+
)
|
|
155
|
+
from .parsers import (
|
|
156
|
+
parse_amount as parse_amount,
|
|
157
|
+
)
|
|
158
|
+
from .parsers import (
|
|
159
|
+
parse_bool as parse_bool,
|
|
160
|
+
)
|
|
161
|
+
from .parsers import (
|
|
162
|
+
parse_datetime as parse_datetime,
|
|
163
|
+
)
|
|
164
|
+
from .parsers import (
|
|
165
|
+
parse_percent as parse_percent,
|
|
166
|
+
)
|
|
167
|
+
from .parsers import (
|
|
168
|
+
parse_size as parse_size,
|
|
169
|
+
)
|
|
170
|
+
from .parsers import (
|
|
171
|
+
parse_slug as parse_slug,
|
|
172
|
+
)
|
|
173
|
+
from .parsers import (
|
|
174
|
+
parse_timedelta as parse_timedelta,
|
|
175
|
+
)
|
|
176
|
+
from .types import SocialLink as SocialLink
|
|
177
|
+
from .validators import (
|
|
178
|
+
valid_bic as valid_bic,
|
|
179
|
+
)
|
|
180
|
+
from .validators import (
|
|
181
|
+
valid_bool as valid_bool,
|
|
182
|
+
)
|
|
183
|
+
from .validators import (
|
|
184
|
+
valid_cif as valid_cif,
|
|
185
|
+
)
|
|
186
|
+
from .validators import (
|
|
187
|
+
valid_date as valid_date,
|
|
188
|
+
)
|
|
189
|
+
from .validators import (
|
|
190
|
+
valid_datetime as valid_datetime,
|
|
191
|
+
)
|
|
192
|
+
from .validators import (
|
|
193
|
+
valid_dni as valid_dni,
|
|
194
|
+
)
|
|
195
|
+
from .validators import (
|
|
196
|
+
valid_domain as valid_domain,
|
|
197
|
+
)
|
|
198
|
+
from .validators import (
|
|
199
|
+
valid_duration as valid_duration,
|
|
200
|
+
)
|
|
201
|
+
from .validators import (
|
|
202
|
+
valid_email as valid_email,
|
|
203
|
+
)
|
|
204
|
+
from .validators import (
|
|
205
|
+
valid_filename as valid_filename,
|
|
206
|
+
)
|
|
207
|
+
from .validators import (
|
|
208
|
+
valid_hex_color as valid_hex_color,
|
|
209
|
+
)
|
|
210
|
+
from .validators import (
|
|
211
|
+
valid_iban as valid_iban,
|
|
212
|
+
)
|
|
213
|
+
from .validators import (
|
|
214
|
+
valid_ip as valid_ip,
|
|
215
|
+
)
|
|
216
|
+
from .validators import (
|
|
217
|
+
valid_json as valid_json,
|
|
218
|
+
)
|
|
219
|
+
from .validators import (
|
|
220
|
+
valid_mac as valid_mac,
|
|
221
|
+
)
|
|
222
|
+
from .validators import (
|
|
223
|
+
valid_nie as valid_nie,
|
|
224
|
+
)
|
|
225
|
+
from .validators import (
|
|
226
|
+
valid_nif as valid_nif,
|
|
227
|
+
)
|
|
228
|
+
from .validators import (
|
|
229
|
+
valid_passport as valid_passport,
|
|
230
|
+
)
|
|
231
|
+
from .validators import (
|
|
232
|
+
valid_password as valid_password,
|
|
233
|
+
)
|
|
234
|
+
from .validators import (
|
|
235
|
+
valid_phone as valid_phone,
|
|
236
|
+
)
|
|
237
|
+
from .validators import (
|
|
238
|
+
valid_port as valid_port,
|
|
239
|
+
)
|
|
240
|
+
from .validators import (
|
|
241
|
+
valid_postal_code as valid_postal_code,
|
|
242
|
+
)
|
|
243
|
+
from .validators import (
|
|
244
|
+
valid_slug as valid_slug,
|
|
245
|
+
)
|
|
246
|
+
from .validators import (
|
|
247
|
+
valid_social_url as valid_social_url,
|
|
248
|
+
)
|
|
249
|
+
from .validators import (
|
|
250
|
+
valid_url as valid_url,
|
|
251
|
+
)
|
|
252
|
+
from .validators import (
|
|
253
|
+
valid_uuid as valid_uuid,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
__all__ = [
|
|
257
|
+
"checkers",
|
|
258
|
+
"converters",
|
|
259
|
+
"getters",
|
|
260
|
+
"parsers",
|
|
261
|
+
"types",
|
|
262
|
+
"validators",
|
|
263
|
+
]
|
acodo/utils/checkers.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Comprobaciones: verifican que los objetos cumplan los requisitos.
|
|
2
|
+
|
|
3
|
+
Las funciones `has_*` devuelven `bool`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Collection, Hashable, Iterable, Mapping
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Comprobaciones
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def has_required_attrs(raw: object, required_attrs: Iterable[str]) -> bool:
|
|
21
|
+
"""Comprueba que el objeto tiene todos los atributos requeridos.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
raw: Objeto a verificar.
|
|
25
|
+
required_attrs: Nombres de los atributos requeridos.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
`True` si el objeto tiene todos los atributos requeridos.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> has_required_attrs("texto", ["upper", "lower"])
|
|
32
|
+
True
|
|
33
|
+
>>> has_required_attrs("texto", ["append"])
|
|
34
|
+
False
|
|
35
|
+
"""
|
|
36
|
+
return all(hasattr(raw, attr) for attr in required_attrs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_required_fields(
|
|
40
|
+
raw: Collection[Any],
|
|
41
|
+
required_fields: Iterable[Any],
|
|
42
|
+
) -> bool:
|
|
43
|
+
"""Comprueba que la colección tiene todos los campos requeridos.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
raw: Colección a verificar.
|
|
47
|
+
required_fields: Campos requeridos.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
`True` si la colección tiene todos los campos requeridos.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
>>> has_required_fields(["a", "b", "c"], ["a", "c"])
|
|
54
|
+
True
|
|
55
|
+
>>> has_required_fields({"a", "b"}, ["a", "z"])
|
|
56
|
+
False
|
|
57
|
+
"""
|
|
58
|
+
return all(field in raw for field in required_fields)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def has_required_keys(
|
|
62
|
+
raw: Mapping[Hashable, Any],
|
|
63
|
+
required_keys: Iterable[Hashable],
|
|
64
|
+
) -> bool:
|
|
65
|
+
"""Comprueba que el diccionario tiene todas las llaves requeridas.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
raw: Diccionario a verificar.
|
|
69
|
+
required_keys: Llaves requeridas.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
`True` si el diccionario tiene todas las llaves requeridas.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
>>> has_required_keys({"name": "Ana", "age": 30}, ["name"])
|
|
76
|
+
True
|
|
77
|
+
>>> has_required_keys({"name": "Ana"}, ["name", "age"])
|
|
78
|
+
False
|
|
79
|
+
"""
|
|
80
|
+
return all(key in raw for key in required_keys)
|