aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-19 14:24:13 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-19 14:24:13 +0200
commit86188385cbdae1ee90c1dca7a7b9db2edef1ecd4 (patch)
treecc01153f05e84ad6cd34556caecd3f4bc335d0bd /packages/meshbay-hub/src/meshbay_hub/api
parentd2495a2c4b89fbbfc18cefec83ae96cabdd745e2 (diff)
downloadmeshbay-86188385cbdae1ee90c1dca7a7b9db2edef1ecd4.tar.gz
style: ruff's own fixes, mechanically applied
`ruff check .` had gone unrun long enough to report 568 errors, which is the same as having no linter: the next real finding would have been invisible in the noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the 98 it cannot fix are the next commit. What actually changed: import sorting (225), imports nobody used (87, none of them a re-export — no `__init__.py` is touched, which was the one way this could have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69) and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12 this project requires, `Optional[X]` to `X | None` (24), and f-strings with nothing to interpolate (19). Checked rather than assumed: every module in the three packages still imports, and the suite is 2893 passed — the same count, test for test, as the merge before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/admin.py5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/deps.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/federation.py12
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/groups.py21
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/hub.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/moderation.py1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/nodes.py10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/notifications.py6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/revocation.py20
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/signaling.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py49
11 files changed, 71 insertions, 61 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
index 7ca1e68..d8e13e9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
@@ -6,19 +6,18 @@ Separate from moderation.py (which handles public reporting and content blocklis
"""
import logging
-from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_hub.auth import decrypt_email
+from meshbay_hub import hub_settings
from meshbay_hub.api.deps import require_admin, require_moderator, user_is_admin
from meshbay_hub.api.revocation import get_connected_node_count, is_node_connected
+from meshbay_hub.auth import decrypt_email
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User
-from meshbay_hub import hub_settings
log = logging.getLogger(__name__)
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/deps.py b/packages/meshbay-hub/src/meshbay_hub/api/deps.py
index 907a481..501be9d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/deps.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/deps.py
@@ -8,8 +8,8 @@ JWT scope enforcement:
"""
from fastapi import Depends, Header, HTTPException, status
-from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub.auth import decode_access_token
from meshbay_hub.db.engine import get_db
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py
index 9e252c6..755639e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py
@@ -23,18 +23,18 @@ Protocol version: MHP 0.1
import logging
import time
import uuid
+from datetime import UTC
import jwt
-from fastapi import APIRouter, Depends, HTTPException, Header
+from fastapi import APIRouter, Depends, Header, HTTPException
+from meshbay_common import MHP_VERSION
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_common import MHP_VERSION
from meshbay_hub import __version__, hub_settings
from meshbay_hub.api.deps import require_admin
-from meshbay_hub.auth import (
- hub_id, hub_private_key_pem, hub_public_key_pem)
+from meshbay_hub.auth import hub_id, hub_private_key_pem, hub_public_key_pem
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import FederatedGroup, Group, HubPeer, User
@@ -244,8 +244,8 @@ async def receive_directory(
if len(body.groups) > MAX_FEDERATED_GROUPS_PER_PUSH:
raise HTTPException(status_code=413, detail="Too many groups in one push")
- from datetime import datetime, timezone
- now = datetime.now(timezone.utc)
+ from datetime import datetime
+ now = datetime.now(UTC)
have = await db.scalar(
select(func.count()).select_from(FederatedGroup)
.where(FederatedGroup.source_hub == sender)) or 0
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
index 72ba194..3a11345 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
@@ -1,22 +1,27 @@
"""Group endpoints — /v1/groups/*"""
import re
+from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
-from datetime import datetime, timezone
from sqlalchemy import func, or_, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub import hub_settings, mail
-from meshbay_hub.auth import decrypt_email
from meshbay_hub.api.deps import get_current_user, require_user_scope
from meshbay_hub.api.middleware import limiter
from meshbay_hub.api.netutil import client_ip
+from meshbay_hub.auth import decrypt_email
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import (
- FederatedGroup, Group, GroupMember, IPLog, SwarmSource, User,
+ FederatedGroup,
+ Group,
+ GroupMember,
+ IPLog,
+ SwarmSource,
+ User,
)
router = APIRouter(prefix="/v1/groups", tags=["groups"])
@@ -97,7 +102,7 @@ async def touch_group_activity(
await db.execute(
update(Group)
.where(Group.id == group_id)
- .values(last_activity_at=datetime.now(timezone.utc)))
+ .values(last_activity_at=datetime.now(UTC)))
await db.commit()
return {"ok": True}
@@ -261,9 +266,9 @@ async def swarm_register(
detail="endpoint must be '<webrtc|quic>:<port>' — a port on the "
"registering node, not an address")
- from datetime import datetime, timezone
+ from datetime import datetime
existing = await db.get(SwarmSource, (body.content_hash, current_user.id))
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
if existing:
existing.endpoint = body.endpoint
existing.last_seen = now
@@ -297,8 +302,8 @@ async def swarm_sources(
Authenticated (H7): an open endpoint lets anyone probe whether a given file
exists anywhere in the network and which node holds it.
"""
- from datetime import datetime, timezone, timedelta
- cutoff = datetime.now(timezone.utc) - timedelta(minutes=30)
+ from datetime import datetime, timedelta
+ cutoff = datetime.now(UTC) - timedelta(minutes=30)
result = await db.execute(
select(SwarmSource)
.where(
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
index 94e9b3c..cab60c8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
@@ -1,9 +1,9 @@
"""Hub info endpoints — /v1/hub/*"""
from fastapi import APIRouter, Depends
+from meshbay_common import MHP_VERSION, MNP_VERSION
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_common import MNP_VERSION, MHP_VERSION
from meshbay_hub import __version__, hub_settings
from meshbay_hub.api import federation
from meshbay_hub.auth import hub_public_key_pem
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py
index ee10cbc..35c688c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py
@@ -23,7 +23,6 @@ Node integration:
"""
import logging
-from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
index 83b60f2..7478173 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
@@ -1,20 +1,20 @@
"""Node endpoints — /v1/nodes/*"""
-from datetime import datetime, timezone
import base64
import time
+from datetime import UTC, datetime
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_hub.auth import issue_access_token
from meshbay_hub.api.deps import get_current_user
from meshbay_hub.api.middleware import limiter
from meshbay_hub.api.netutil import client_ip
+from meshbay_hub.auth import issue_access_token
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import GroupMember, IPLog, Node, User
@@ -156,7 +156,7 @@ async def announce_node(
if node is not None:
node.endpoint_hint = body.endpoint_hint
node.observed_ip = seen_from
- node.last_seen = datetime.now(timezone.utc)
+ node.last_seen = datetime.now(UTC)
db.add(IPLog(user_id=current_user.id, event="node_announce",
ip_address=seen_from, detail=body.endpoint_hint))
await db.commit()
@@ -182,7 +182,7 @@ async def announce_node(
pk_node=body.pk_node,
endpoint_hint=body.endpoint_hint,
observed_ip=seen_from,
- last_seen=datetime.now(timezone.utc),
+ last_seen=datetime.now(UTC),
)
db.add(node)
db.add(IPLog(
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
index b5783ab..d96ec18 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
@@ -19,9 +19,9 @@ migration for no gain, and `unread_only` stays because it is what an older
interface asks for and it still answers correctly — every row is unread.
"""
-from fastapi import APIRouter, Depends, HTTPException, Query
-from datetime import datetime, timezone
+from datetime import UTC, datetime
+from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -182,7 +182,7 @@ async def create_notification(
existing.detail = detail
existing.link = link
existing.read = False
- existing.created_at = datetime.now(timezone.utc)
+ existing.created_at = datetime.now(UTC)
await db.flush()
return existing
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py
index 1f1f5c5..f8cae8a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py
@@ -26,23 +26,21 @@ and close active connections for that user.
"""
import asyncio
-import base64
import json
import logging
import time
import uuid
-from datetime import datetime, timezone
-from typing import Any
+from datetime import UTC, datetime
+import jwt
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect
+from meshbay_common.background import spawn
from pydantic import BaseModel
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
-import jwt
-from meshbay_common.background import spawn
-from meshbay_hub.auth import hub_public_key_pem, decode_access_token
from meshbay_hub.api.deps import get_current_user, require_admin
+from meshbay_hub.auth import decode_access_token
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User
@@ -143,7 +141,7 @@ async def _mark_hosted(group_ids: list[str]) -> None:
await db.execute(
update(Group)
.where(Group.id.in_(group_ids), Group.hosted_at.is_(None))
- .values(hosted_at=datetime.now(timezone.utc)))
+ .values(hosted_at=datetime.now(UTC)))
await db.commit()
except Exception as e:
# A group that stays unhosted in the table is visible to its owner and
@@ -169,7 +167,7 @@ async def broadcast_revocation(token: str) -> int:
def _sign_revocation(target: str, target_id: str, reason: str) -> str:
"""Issue a signed revocation token (JWT EdDSA)."""
- from meshbay_hub.auth import _hub_sk_pem, _hub_id
+ from meshbay_hub.auth import _hub_id, _hub_sk_pem
now = int(time.time())
payload = {
"type": "revocation",
@@ -208,9 +206,9 @@ async def _handle_chat_notify(group_id: str, sender_name: str, sender_user_id: s
(node_id or "?")[:8])
return
try:
- from meshbay_hub.db.engine import get_session_factory
- from meshbay_hub.db.models import GroupMember, Group
from meshbay_hub.api.notifications import create_notification
+ from meshbay_hub.db.engine import get_session_factory
+ from meshbay_hub.db.models import Group, GroupMember
async with get_session_factory()() as db:
group = await db.get(Group, group_id)
@@ -478,7 +476,7 @@ async def notify_incoming(
try:
await asyncio.wait_for(event.wait(), timeout=5.0)
- except asyncio.TimeoutError:
+ except TimeoutError:
raise HTTPException(status_code=504, detail="Node did not respond in time")
finally:
_punch_events.pop(node_id, None)
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
index bc03b45..8a10822 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
@@ -25,8 +25,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub import hub_settings
from meshbay_hub.api.deps import get_current_user
from meshbay_hub.api.middleware import limiter
-from meshbay_hub.db.engine import get_db
from meshbay_hub.api.netutil import client_ip
+from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import Group, GroupMember, IPLog, User
log = logging.getLogger(__name__)
@@ -167,7 +167,7 @@ async def webrtc_offer(
try:
answer = await asyncio.wait_for(answer_future, timeout=15.0)
- except asyncio.TimeoutError:
+ except TimeoutError:
raise HTTPException(
status_code=504, detail="Node did not respond with WebRTC answer")
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 0394f53..ee8aabc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -6,7 +6,7 @@ import re
import secrets
import time
import uuid
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from fastapi import APIRouter, Depends, HTTPException, Request
@@ -33,8 +33,17 @@ from meshbay_hub.auth import (
from meshbay_hub.config import HubConfig
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import (
- EmailVerification, Group, GroupMember, IPLog, Node, Notification,
- RefreshToken, SwarmSource, User, UserDevice, UserPreference,
+ EmailVerification,
+ Group,
+ GroupMember,
+ IPLog,
+ Node,
+ Notification,
+ RefreshToken,
+ SwarmSource,
+ User,
+ UserDevice,
+ UserPreference,
)
log = logging.getLogger(__name__)
@@ -61,7 +70,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat
renewals of a tab that is being used.
"""
limits = await hub_settings.session_limits(db)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
idle = max(limits["refresh_idle_hours"] * 3600, _ttl() + 3600)
started = now
if family_id is not None:
@@ -69,7 +78,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat
select(func.min(RefreshToken.created_at))
.where(RefreshToken.family_id == family_id))
if first is not None:
- started = first if first.tzinfo else first.replace(tzinfo=timezone.utc)
+ started = first if first.tzinfo else first.replace(tzinfo=UTC)
return min(now + timedelta(seconds=idle),
started + timedelta(hours=limits["max_hours"]))
@@ -193,7 +202,7 @@ async def register(
EmailVerification.user_id == found.id,
EmailVerification.purpose == "registration",
EmailVerification.created_at
- > datetime.now(timezone.utc)
+ > datetime.now(UTC)
- timedelta(seconds=resend_cooldown),
))
if not recent.first():
@@ -270,7 +279,7 @@ async def _create_and_send_verification(
code=code,
purpose="registration",
user_id=user.id,
- expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL),
+ expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL),
))
await db.flush()
await mail.send_off_loop(
@@ -292,7 +301,7 @@ async def verify_email(
):
"""Verify a registration email with the code received by mail."""
eh = hash_email_blind(body.email)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(
select(EmailVerification).where(
@@ -306,7 +315,7 @@ async def verify_email(
raise HTTPException(status_code=404,
detail="No pending verification for this email")
- if verif.expires_at.replace(tzinfo=timezone.utc) < now:
+ if verif.expires_at.replace(tzinfo=UTC) < now:
raise HTTPException(status_code=410, detail="Verification code expired")
if verif.attempts >= VERIFICATION_MAX_ATTEMPTS:
@@ -603,7 +612,7 @@ async def device_auth(
await db.commit()
raise HTTPException(status_code=401, detail="Invalid signature")
- matched.last_seen = datetime.now(timezone.utc)
+ matched.last_seen = datetime.now(UTC)
memberships = await db.execute(
select(GroupMember.group_id).where(GroupMember.user_id == user.id))
@@ -650,7 +659,7 @@ async def token_refresh(
await db.commit()
raise HTTPException(status_code=401, detail="Token reuse detected — family revoked")
- if rt.expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
+ if rt.expires_at.replace(tzinfo=UTC) < datetime.now(UTC):
raise HTTPException(status_code=401, detail="Expired refresh token")
user = await db.get(User, rt.user_id)
@@ -659,7 +668,7 @@ async def token_refresh(
# The family's first sign-in was longer ago than any session may last.
expires_at = await _refresh_expiry(db, rt.family_id)
- if expires_at <= datetime.now(timezone.utc):
+ if expires_at <= datetime.now(UTC):
await db.execute(
update(RefreshToken)
.where(RefreshToken.family_id == rt.family_id)
@@ -779,7 +788,7 @@ async def update_profile(
cooldown = await hub_settings.get_int(
db, "mail.email_change_cooldown",
hub_settings.mail_default("email_change_cooldown"))
- since = datetime.now(timezone.utc) - timedelta(seconds=cooldown)
+ since = datetime.now(UTC) - timedelta(seconds=cooldown)
recent = await db.execute(
select(IPLog).where(
IPLog.user_id == current_user.id,
@@ -820,7 +829,7 @@ async def update_profile(
code=code,
purpose="email_change",
user_id=current_user.id,
- expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL),
+ expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL),
))
db.add(IPLog(user_id=current_user.id, event="email_change_request",
ip_address=client_ip(request)))
@@ -860,7 +869,7 @@ async def verify_email_change(
db: AsyncSession = Depends(get_db),
):
"""Confirm an email change with the code sent to the new address."""
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(
select(EmailVerification).where(
@@ -874,7 +883,7 @@ async def verify_email_change(
raise HTTPException(status_code=404,
detail="No pending email change")
- if verif.expires_at.replace(tzinfo=timezone.utc) < now:
+ if verif.expires_at.replace(tzinfo=UTC) < now:
raise HTTPException(status_code=410, detail="Verification code expired")
if verif.attempts >= VERIFICATION_MAX_ATTEMPTS:
@@ -1090,7 +1099,7 @@ async def password_reset_request(
EmailVerification.user_id == user.id,
EmailVerification.purpose == "password_reset",
EmailVerification.created_at
- > datetime.now(timezone.utc) - timedelta(seconds=reset_cooldown),
+ > datetime.now(UTC) - timedelta(seconds=reset_cooldown),
))
if recent.first():
return {"status": "sent_if_exists"}
@@ -1110,7 +1119,7 @@ async def password_reset_request(
code=code,
purpose="password_reset",
user_id=user.id,
- expires_at=datetime.now(timezone.utc)
+ expires_at=datetime.now(UTC)
+ timedelta(seconds=PASSWORD_RESET_TTL),
))
db.add(IPLog(user_id=user.id, event="password_reset_request",
@@ -1141,7 +1150,7 @@ async def password_reset(
request: Request,
db: AsyncSession = Depends(get_db),
):
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(select(User).where(User.username == body.username))
user = result.scalar_one_or_none()
if not user:
@@ -1157,7 +1166,7 @@ async def password_reset(
if not verif:
raise HTTPException(status_code=404,
detail="No pending reset for this account")
- if verif.expires_at.replace(tzinfo=timezone.utc) < now:
+ if verif.expires_at.replace(tzinfo=UTC) < now:
raise HTTPException(status_code=410, detail="Reset code expired")
if verif.attempts >= VERIFICATION_MAX_ATTEMPTS:
raise HTTPException(status_code=429, detail="Too many attempts")