chore(backend): deprecation sweep — Pydantic V1 -> V2, utcnow -> tz-aware

Two low-risk Pydantic V1 idioms that will be hard errors in V3:
  - Query(regex=...) -> Query(pattern=...) (audit_routes, control_generator_routes)
  - class Config: from_attributes=True -> model_config = ConfigDict(...)
    in source_policy_router.py (schemas.py is intentionally skipped — it is
    the Phase 1 schema-split target and the ConfigDict conversion is most
    efficient to do during that split).

Naive -> aware datetime sweep across 47 files:
  - datetime.utcnow() -> datetime.now(timezone.utc)
  - default=datetime.utcnow -> default=lambda: datetime.now(timezone.utc)
  - onupdate=datetime.utcnow -> onupdate=lambda: datetime.now(timezone.utc)

All SQLAlchemy DateTime columns in the project already declare
timezone=True, so the DB schema expects aware datetimes. Before this
commit, the in-Python side was generating naive values and the driver
was silently coercing them. This is a latent-bug fix, not a behavior
change at the DB boundary.

Verified:
  - 173/173 pytest compliance/tests/ pass (same as baseline)
  - tests/contracts/test_openapi_baseline.py passes (360 paths,
    484 operations unchanged)
  - DeprecationWarning count dropped from 158 -> 35

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-07 13:09:59 +02:00
parent 512b7a0f6c
commit cb90d0db0c
47 changed files with 260 additions and 261 deletions

View File

@@ -10,7 +10,7 @@ import pytest
import uuid
import os
import sys
from datetime import datetime
from datetime import datetime, timezone
from unittest.mock import MagicMock
from fastapi import FastAPI
@@ -51,7 +51,7 @@ _RawSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@event.listens_for(engine, "connect")
def _register_sqlite_functions(dbapi_conn, connection_record):
"""Register PostgreSQL-compatible functions for SQLite."""
dbapi_conn.create_function("NOW", 0, lambda: datetime.utcnow().isoformat())
dbapi_conn.create_function("NOW", 0, lambda: datetime.now(timezone.utc).isoformat())
TENANT_ID = "default"

View File

@@ -6,7 +6,7 @@ Pattern: app.dependency_overrides[get_db] for FastAPI DI.
import uuid
import os
import sys
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import pytest
from fastapi import FastAPI
@@ -75,7 +75,7 @@ def db_session():
def _create_dsr_in_db(db, **kwargs):
"""Helper to create a DSR directly in DB."""
now = datetime.utcnow()
now = datetime.now(timezone.utc)
defaults = {
"tenant_id": uuid.UUID(TENANT_ID),
"request_number": f"DSR-2026-{str(uuid.uuid4())[:6].upper()}",
@@ -241,8 +241,8 @@ class TestListDSR:
assert len(data["requests"]) == 2
def test_list_overdue_only(self, db_session):
_create_dsr_in_db(db_session, deadline_at=datetime.utcnow() - timedelta(days=5), status="processing")
_create_dsr_in_db(db_session, deadline_at=datetime.utcnow() + timedelta(days=20), status="processing")
_create_dsr_in_db(db_session, deadline_at=datetime.now(timezone.utc) - timedelta(days=5), status="processing")
_create_dsr_in_db(db_session, deadline_at=datetime.now(timezone.utc) + timedelta(days=20), status="processing")
resp = client.get("/api/compliance/dsr?overdue_only=true", headers=HEADERS)
assert resp.status_code == 200
@@ -339,7 +339,7 @@ class TestDSRStats:
_create_dsr_in_db(db_session, status="intake", request_type="access")
_create_dsr_in_db(db_session, status="processing", request_type="erasure")
_create_dsr_in_db(db_session, status="completed", request_type="access",
completed_at=datetime.utcnow())
completed_at=datetime.now(timezone.utc))
resp = client.get("/api/compliance/dsr/stats", headers=HEADERS)
assert resp.status_code == 200
@@ -561,9 +561,9 @@ class TestDeadlineProcessing:
def test_process_deadlines_with_overdue(self, db_session):
_create_dsr_in_db(db_session, status="processing",
deadline_at=datetime.utcnow() - timedelta(days=5))
deadline_at=datetime.now(timezone.utc) - timedelta(days=5))
_create_dsr_in_db(db_session, status="processing",
deadline_at=datetime.utcnow() + timedelta(days=20))
deadline_at=datetime.now(timezone.utc) + timedelta(days=20))
resp = client.post("/api/compliance/dsr/deadlines/process", headers=HEADERS)
assert resp.status_code == 200
@@ -609,7 +609,7 @@ class TestDSRTemplates:
subject="Bestaetigung",
body_html="<p>Test</p>",
status="published",
published_at=datetime.utcnow(),
published_at=datetime.now(timezone.utc),
)
db_session.add(v)
db_session.commit()

View File

@@ -7,7 +7,7 @@ Consent widerrufen, Statistiken.
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from datetime import datetime, timezone
import uuid
@@ -25,7 +25,7 @@ def make_catalog(tenant_id='test-tenant'):
rec.tenant_id = tenant_id
rec.selected_data_point_ids = ['dp-001', 'dp-002']
rec.custom_data_points = []
rec.updated_at = datetime.utcnow()
rec.updated_at = datetime.now(timezone.utc)
return rec
@@ -34,7 +34,7 @@ def make_company(tenant_id='test-tenant'):
rec.id = uuid.uuid4()
rec.tenant_id = tenant_id
rec.data = {'company_name': 'Test GmbH', 'email': 'datenschutz@test.de'}
rec.updated_at = datetime.utcnow()
rec.updated_at = datetime.now(timezone.utc)
return rec
@@ -47,7 +47,7 @@ def make_cookies(tenant_id='test-tenant'):
{'id': 'analytics', 'name': 'Analyse', 'isRequired': False, 'defaultEnabled': False},
]
rec.config = {'position': 'bottom', 'style': 'bar'}
rec.updated_at = datetime.utcnow()
rec.updated_at = datetime.now(timezone.utc)
return rec
@@ -58,13 +58,13 @@ def make_consent(tenant_id='test-tenant', user_id='user-001', data_point_id='dp-
rec.user_id = user_id
rec.data_point_id = data_point_id
rec.granted = granted
rec.granted_at = datetime.utcnow()
rec.granted_at = datetime.now(timezone.utc)
rec.revoked_at = None
rec.consent_version = '1.0'
rec.source = 'website'
rec.ip_address = None
rec.user_agent = None
rec.created_at = datetime.utcnow()
rec.created_at = datetime.now(timezone.utc)
return rec
@@ -263,7 +263,7 @@ class TestConsentDB:
user_id='user-001',
data_point_id='dp-marketing',
granted=True,
granted_at=datetime.utcnow(),
granted_at=datetime.now(timezone.utc),
consent_version='1.0',
source='website',
)
@@ -276,13 +276,13 @@ class TestConsentDB:
consent = make_consent()
assert consent.revoked_at is None
consent.revoked_at = datetime.utcnow()
consent.revoked_at = datetime.now(timezone.utc)
assert consent.revoked_at is not None
def test_cannot_revoke_already_revoked(self):
"""Should not be possible to revoke an already revoked consent."""
consent = make_consent()
consent.revoked_at = datetime.utcnow()
consent.revoked_at = datetime.now(timezone.utc)
# Simulate the guard logic from the route
already_revoked = consent.revoked_at is not None
@@ -315,7 +315,7 @@ class TestConsentStats:
make_consent(user_id='user-2', data_point_id='dp-1', granted=True),
]
# Revoke one
consents[1].revoked_at = datetime.utcnow()
consents[1].revoked_at = datetime.now(timezone.utc)
total = len(consents)
active = sum(1 for c in consents if c.granted and not c.revoked_at)
@@ -334,7 +334,7 @@ class TestConsentStats:
make_consent(user_id='user-2', granted=True),
make_consent(user_id='user-3', granted=True),
]
consents[2].revoked_at = datetime.utcnow() # user-3 revoked
consents[2].revoked_at = datetime.now(timezone.utc) # user-3 revoked
unique_users = len(set(c.user_id for c in consents))
users_with_active = len(set(c.user_id for c in consents if c.granted and not c.revoked_at))
@@ -501,7 +501,7 @@ class TestConsentHistoryTracking:
from compliance.db.einwilligungen_models import EinwilligungenConsentHistoryDB
consent = make_consent()
consent.revoked_at = datetime.utcnow()
consent.revoked_at = datetime.now(timezone.utc)
entry = EinwilligungenConsentHistoryDB(
consent_id=consent.id,
tenant_id=consent.tenant_id,
@@ -516,7 +516,7 @@ class TestConsentHistoryTracking:
entry_id = _uuid.uuid4()
consent_id = _uuid.uuid4()
now = datetime.utcnow()
now = datetime.now(timezone.utc)
row = {
"id": str(entry_id),

View File

@@ -13,7 +13,7 @@ Run with: cd backend-compliance && python3 -m pytest tests/test_isms_routes.py -
import os
import sys
import pytest
from datetime import date, datetime
from datetime import date, datetime, timezone
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -40,7 +40,7 @@ def _set_sqlite_pragma(dbapi_conn, connection_record):
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
dbapi_conn.create_function("NOW", 0, lambda: datetime.utcnow().isoformat())
dbapi_conn.create_function("NOW", 0, lambda: datetime.now(timezone.utc).isoformat())
app = FastAPI()

View File

@@ -7,7 +7,7 @@ Rejection-Flow, approval history.
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from datetime import datetime, timezone
import uuid
@@ -27,7 +27,7 @@ def make_document(type='privacy_policy', name='Datenschutzerklärung', tenant_id
doc.name = name
doc.description = 'Test description'
doc.mandatory = False
doc.created_at = datetime.utcnow()
doc.created_at = datetime.now(timezone.utc)
doc.updated_at = None
return doc
@@ -46,7 +46,7 @@ def make_version(document_id=None, version='1.0', status='draft', title='Test Ve
v.approved_by = None
v.approved_at = None
v.rejection_reason = None
v.created_at = datetime.utcnow()
v.created_at = datetime.now(timezone.utc)
v.updated_at = None
return v
@@ -58,7 +58,7 @@ def make_approval(version_id=None, action='created'):
a.action = action
a.approver = 'admin@test.de'
a.comment = None
a.created_at = datetime.utcnow()
a.created_at = datetime.now(timezone.utc)
return a
@@ -179,7 +179,7 @@ class TestVersionToResponse:
from compliance.api.legal_document_routes import _version_to_response
v = make_version(status='approved')
v.approved_by = 'dpo@company.de'
v.approved_at = datetime.utcnow()
v.approved_at = datetime.now(timezone.utc)
resp = _version_to_response(v)
assert resp.status == 'approved'
assert resp.approved_by == 'dpo@company.de'
@@ -254,7 +254,7 @@ class TestApprovalWorkflow:
# Step 2: Approve
mock_db.reset_mock()
_transition(mock_db, str(v.id), ['review'], 'approved', 'approved', 'dpo', 'Korrekt',
extra_updates={'approved_by': 'dpo', 'approved_at': datetime.utcnow()})
extra_updates={'approved_by': 'dpo', 'approved_at': datetime.now(timezone.utc)})
assert v.status == 'approved'
# Step 3: Publish

View File

@@ -5,7 +5,7 @@ Tests for Legal Document extended routes (User Consents, Audit Log, Cookie Categ
import uuid
import os
import sys
from datetime import datetime
from datetime import datetime, timezone
import pytest
from fastapi import FastAPI
@@ -103,7 +103,7 @@ def _publish_version(version_id):
v = db.query(LegalDocumentVersionDB).filter(LegalDocumentVersionDB.id == vid).first()
v.status = "published"
v.approved_by = "admin"
v.approved_at = datetime.utcnow()
v.approved_at = datetime.now(timezone.utc)
db.commit()
db.refresh(v)
result = {"id": str(v.id), "status": v.status}

View File

@@ -15,7 +15,7 @@ import pytest
import uuid
import os
import sys
from datetime import datetime
from datetime import datetime, timezone
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -40,7 +40,7 @@ TENANT_ID = "default"
@event.listens_for(engine, "connect")
def _register_sqlite_functions(dbapi_conn, connection_record):
dbapi_conn.create_function("NOW", 0, lambda: datetime.utcnow().isoformat())
dbapi_conn.create_function("NOW", 0, lambda: datetime.now(timezone.utc).isoformat())
class _DictRow(dict):

View File

@@ -186,7 +186,7 @@ class TestActivityToResponse:
act.next_review_at = kwargs.get("next_review_at", None)
act.created_by = kwargs.get("created_by", None)
act.dsfa_id = kwargs.get("dsfa_id", None)
act.created_at = datetime.utcnow()
act.created_at = datetime.now(timezone.utc)
act.updated_at = None
return act
@@ -330,7 +330,7 @@ class TestVVTConsolidationResponse:
act.next_review_at = kwargs.get("next_review_at", None)
act.created_by = kwargs.get("created_by", None)
act.dsfa_id = kwargs.get("dsfa_id", None)
act.created_at = datetime.utcnow()
act.created_at = datetime.now(timezone.utc)
act.updated_at = None
return act

View File

@@ -10,7 +10,7 @@ Verifies that:
import pytest
import uuid
from unittest.mock import MagicMock, AsyncMock, patch
from datetime import datetime
from datetime import datetime, timezone
from fastapi import HTTPException
from fastapi.testclient import TestClient
@@ -144,8 +144,8 @@ def _make_activity(tenant_id, vvt_id="VVT-001", name="Test", **kwargs):
act.next_review_at = None
act.created_by = "system"
act.dsfa_id = None
act.created_at = datetime.utcnow()
act.updated_at = datetime.utcnow()
act.created_at = datetime.now(timezone.utc)
act.updated_at = datetime.now(timezone.utc)
return act