File size: 1,846 Bytes
db521fa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | import hashlib
import hmac
import re
import secrets
import string
from datetime import datetime, timezone
from typing import Any, Optional
EMAIL_RE = re.compile(r'^[\w.+-]+@[\w-]+\.[\w.-]+$')
def hash_password(pw: str, salt: Optional[str] = None) -> tuple[str, str]:
if salt is None:
salt = secrets.token_hex(16)
digest = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), 200_000)
return digest.hex(), salt
def verify_password(pw: str, digest: str, salt: str) -> bool:
computed, _ = hash_password(pw, salt)
return hmac.compare_digest(computed, digest)
def generate_token(n: int = 32) -> str:
return "".join(secrets.choice(string.ascii_letters + string.digits)
for _ in range(n))
def slugify(text: str, max_len: int = 60) -> str:
text = text.lower().replace(" ", "-")
text = re.sub(r"[^\w-]", "", text)
return text[:max_len].strip("-")
def truncate(text: str, n: int = 120) -> str:
return text if len(text) <= n else text[:n - 3] + "..."
def utcnow() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_bool(v: Any) -> bool:
if isinstance(v, bool):
return v
return str(v).strip().lower() in ("1", "true", "yes", "on")
def chunk(lst: list, size: int) -> list[list]:
return [lst[i:i + size] for i in range(0, len(lst), size)]
def flatten(nested: list) -> list:
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
def deep_merge(base: dict, override: dict) -> dict:
out = dict(base)
for k, v in override.items():
if k in out and isinstance(out[k], dict) and isinstance(v, dict):
out[k] = deep_merge(out[k], v)
else:
out[k] = v
return out
|