Skip to content

Upgrade backend runtime to Python 3.13 and modernize dependencies - #270

Merged
kargig merged 5 commits into
mainfrom
feature/python-upgrade-plan
Oct 16, 2026
Merged

kargig merged 5 commits into
mainfrom
feature/python-upgrade-plan

Conversation

@kargig

@kargig kargig commented Oct 16, 2026

Copy link
Copy Markdown
Owner
  • Upgrade backend base Docker images and test runners to Python 3.13.
  • Publish pre-compiled cp313 and cp314 wheels for octo-deco to GitHub Release deps-v1.
  • Prune unused direct dependencies (celery, redis, google-auth-oauthlib, pyasn1), reducing installed packages from 114 to 99.
  • Modernize dependencies to Python 3.13-compatible versions (pydantic 2.14, anyio 4.15, uvicorn 0.54, orjson 3.13, pillow 12.3, pytest 9).
  • Refactor legacy Pydantic V1 class Config blocks to model_config = ConfigDict(from_attributes=True) across schemas.
  • Adopt itertools.batched(..., strict=False) for C-accelerated batching.
  • Migrate deprecated datetime.utcnow() to timezone-aware datetime.now(UTC) and app.utils.utcnow() across the entire codebase.
  • Add --skip-ssl-verify-server-cert to test MySQL client invocations for Debian 13 (Trixie) TLS compliance with self-signed test certs.
  • Downgrade optional pywebpush import error to warning in Lambda worker.
  • Update GitHub Actions workflow and local development environment to Python 3.13.
  • Verify 100% pass rate across all 1,870 backend tests in Docker.

- Upgrade backend base Docker images and test runners to Python 3.13.
- Publish pre-compiled cp313 and cp314 wheels for octo-deco to GitHub
  Release deps-v1.
- Prune unused direct dependencies (celery, redis, google-auth-oauthlib,
  pyasn1), reducing installed packages from 114 to 99.
- Modernize dependencies to Python 3.13-compatible versions (pydantic
  2.14, anyio 4.15, uvicorn 0.54, orjson 3.13, pillow 12.3, pytest 9).
- Refactor legacy Pydantic V1 class Config blocks to model_config =
  ConfigDict(from_attributes=True) across schemas.
- Adopt itertools.batched(..., strict=False) for C-accelerated batching.
- Migrate deprecated datetime.utcnow() to timezone-aware datetime.now(UTC)
  and app.utils.utcnow() across the entire codebase.
- Add --skip-ssl-verify-server-cert to test MySQL client invocations for
  Debian 13 (Trixie) TLS compliance with self-signed test certs.
- Downgrade optional pywebpush import error to warning in Lambda worker.
- Update GitHub Actions workflow and local development environment to
  Python 3.13.
- Verify 100% pass rate across all 1,870 backend tests in Docker.
@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The SQS batch entry Id is now computed as str(batch_idx * batch_size + idx). This is only equivalent to the previous str(i + idx) if every batch is exactly batch_size long. With batched(..., strict=False) the final batch can be shorter, but since batch_idx increments by 1 per batch and the offset uses the full batch_size, the IDs remain globally unique and monotonic — so this is correct. However, if batch_size is ever changed independently of the actual chunking, IDs could collide. Verify the invariant holds; otherwise no action needed.

for batch_idx, batch in enumerate(batched(tasks, batch_size, strict=False)):
    try:
        entries = []
        for idx, task in enumerate(batch):
            message_body = {
                "notification_id": task["notification_id"],
                "user_email": task["user_email"],
                "notification": task["notification"],
            }

            # Include user_id and unsubscribe_token if provided
            if "user_id" in task and task["user_id"] is not None:
                message_body["user_id"] = task["user_id"]
            if "unsubscribe_token" in task and task["unsubscribe_token"]:
                message_body["unsubscribe_token"] = task["unsubscribe_token"]

            entries.append(
                {
                    "Id": str(batch_idx * batch_size + idx),
Possible Issue

normalize_datetime_to_utc is applied to current_user.updated_at/created_at for the range comparison, but the AuditLogEntry still receives the raw current_user.updated_at/created_at as timestamp. If those columns are naive datetimes while start_date/end_date are timezone-aware, the comparison is now correct but the emitted timestamp may be inconsistent with the rest of the response (which uses aware datetimes). Confirm the model columns are timezone-aware or normalize the emitted timestamp too.

user_updated_at = normalize_datetime_to_utc(current_user.updated_at)
user_created_at = normalize_datetime_to_utc(current_user.created_at)

if (
    user_updated_at
    and user_updated_at >= start_date
    and user_updated_at <= end_date
):
    if user_created_at != user_updated_at:
        audit_entries.append(
            AuditLogEntry(
                timestamp=current_user.updated_at,
                action="UPDATE",
                resource_type="user_profile",
                resource_id=current_user.id,
                details={"updated_fields": "profile_information"},
            )
        )

# Add account creation if within period
if (
    user_created_at
    and user_created_at >= start_date
    and user_created_at <= end_date
):
    audit_entries.append(
        AuditLogEntry(
            timestamp=current_user.created_at,
Possible Issue

target_datetime is now coerced to UTC when naive, but the subsequent comparison target_datetime < now_utc - timedelta(hours=1) assumes the input was UTC. If a client sends a naive local-time string, it will be interpreted as UTC, potentially rejecting valid or accepting invalid requests. This is a behavior change from the previous naive datetime.utcnow() comparison; ensure clients always send UTC (Z or offset) or document the assumption.

target_datetime = datetime.fromisoformat(
    datetime_str.replace("Z", "+00:00")
)
if target_datetime.tzinfo is None:
    target_datetime = target_datetime.replace(tzinfo=UTC)
# Validate date range: only allow up to +2 days ahead
# Round max_future to next hour to allow selecting any hour within 2-day window
now_utc = datetime.now(UTC)
max_future = (now_utc + timedelta(days=2)).replace(
    minute=0, second=0, microsecond=0
) + timedelta(hours=1)
if target_datetime > max_future:
    raise HTTPException(
        status_code=400,
        detail=f"Date/time cannot be more than 2 days ahead. Maximum allowed: {max_future.isoformat()}",
    )
# Don't allow past dates (only current and future up to +2 days)
if target_datetime < now_utc - timedelta(hours=1):

⚠️ Review coverage: The following files were not included in this review because of the token budget:

  • backend/pyproject.toml
  • GEMINI.md

- Fix Ruff lint errors in backend/lambda/email_processor.py (unsorted
  imports, Dict to dict, Optional to | None, and bare except).
- Add field_validator on AuditLogEntry.timestamp to normalize all
  incoming naive database timestamps to UTC, preventing sort errors
  between offset-naive and offset-aware datetimes.
- Format backend/lambda/email_processor.py and backend/tests/test_weather_api.py
  with ruff format to satisfy CI check formatting workflow.
- Fix mock target in test_get_wind_data_single_point to patch
  app.routers.weather.fetch_wind_data_single_point instead of service,
  preventing live external HTTP calls and timeout failures to Open-Meteo.
- Normalize audit log entry timestamps to UTC via field validator to
  prevent offset-naive vs offset-aware comparison errors.
Correct the backend-tests migrations step that still
pointed PYTHONPATH at python3.11 after the 3.13
upgrade.

Add a --warnings flag to the Docker test runner so
DeprecationWarnings can be surfaced on demand, set
pytest-asyncio's fixture loop scope, and filter the
known Starlette/anyio BlockingPortal warning.

Replace remaining datetime.utcnow() usage in tests and
Pydantic .dict() calls in dive site/tag moderation
paths with timezone-aware UTC and model_dump().
- Format the 8 test files modified in commit 1c8e577 to resolve the
  failing GitHub Actions 'Backend Lint & Format' step.
- Fix import sorting, UTC alias modernization, and strict zip handling.
@kargig
kargig merged commit d639fcc into main Oct 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant