From 819ab896bdb336b0f15d24d14f30f3a3217a09f0 Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 5 Feb 2026 07:46:22 +0100 Subject: [PATCH] Fix Python 3.10 compatibility (Issue #269) Replace datetime.UTC with timezone.utc for Python 3.10 support. datetime.UTC was added in Python 3.11, but requirements state 3.10+. Closes #269 --- backend/app/api/routes/pending_uploads.py | 6 ++--- backend/app/main.py | 4 ++-- backend/app/services/github_backup.py | 24 +++++++++---------- backend/app/services/spoolman.py | 4 ++-- .../services/virtual_printer/certificate.py | 8 +++---- .../app/services/virtual_printer/manager.py | 4 ++-- pyproject.toml | 1 + 7 files changed, 26 insertions(+), 25 deletions(-) diff --git a/backend/app/api/routes/pending_uploads.py b/backend/app/api/routes/pending_uploads.py index 83c3c455f..29d48b44e 100644 --- a/backend/app/api/routes/pending_uploads.py +++ b/backend/app/api/routes/pending_uploads.py @@ -1,6 +1,6 @@ """API routes for pending uploads (virtual printer queue mode).""" -from datetime import UTC, datetime +from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Depends, HTTPException @@ -107,7 +107,7 @@ async def archive_all_pending( if archive: pending.status = "archived" pending.archived_id = archive.id - pending.archived_at = datetime.now(UTC) + pending.archived_at = datetime.now(timezone.utc) archived += 1 # Clean up temp file @@ -219,7 +219,7 @@ async def archive_pending_upload( # Update pending record pending.status = "archived" pending.archived_id = archive.id - pending.archived_at = datetime.now(UTC) + pending.archived_at = datetime.now(timezone.utc) if request: pending.tags = request.tags pending.notes = request.notes diff --git a/backend/app/main.py b/backend/app/main.py index 476bd1d79..db4b52564 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,7 @@ import asyncio import logging from contextlib import asynccontextmanager -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone from logging.handlers import RotatingFileHandler @@ -1027,7 +1027,7 @@ async def on_print_start(printer_id: int, data: dict): if existing_archive: # Check if archive is stale (older than 4 hours) - likely a failed/cancelled print # that didn't get properly updated - archive_age = datetime.now(UTC) - existing_archive.created_at.replace(tzinfo=UTC) + archive_age = datetime.now(timezone.utc) - existing_archive.created_at.replace(tzinfo=timezone.utc) if archive_age.total_seconds() > 4 * 60 * 60: # 4 hours logger.warning( f"Found stale 'printing' archive {existing_archive.id} (age: {archive_age}), " diff --git a/backend/app/services/github_backup.py b/backend/app/services/github_backup.py index 64b4f0ec7..3321a9d10 100644 --- a/backend/app/services/github_backup.py +++ b/backend/app/services/github_backup.py @@ -9,7 +9,7 @@ import hashlib import json import logging import re -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone import httpx from sqlalchemy import desc, select @@ -85,19 +85,19 @@ class GitHubBackupService: ) configs = result.scalars().all() - now = datetime.now(UTC) + now = datetime.now(timezone.utc) for config in configs: # Handle both naive (from DB) and aware datetimes next_run = config.next_scheduled_run if next_run and next_run.tzinfo is None: - next_run = next_run.replace(tzinfo=UTC) + next_run = next_run.replace(tzinfo=timezone.utc) if next_run and next_run <= now: logger.info(f"Running scheduled backup for config {config.id}") await self.run_backup(config.id, trigger="scheduled") def _calculate_next_run(self, schedule_type: str, from_time: datetime | None = None) -> datetime: """Calculate the next scheduled run time.""" - now = from_time or datetime.now(UTC) + now = from_time or datetime.now(timezone.utc) interval = SCHEDULE_INTERVALS.get(schedule_type, SCHEDULE_INTERVALS["daily"]) return now + timedelta(seconds=interval) @@ -237,9 +237,9 @@ class GitHubBackupService: if not backup_data: # No data to backup log.status = "skipped" - log.completed_at = datetime.now(UTC) + log.completed_at = datetime.now(timezone.utc) log.error_message = "No data to backup" - config.last_backup_at = datetime.now(UTC) + config.last_backup_at = datetime.now(timezone.utc) config.last_backup_status = "skipped" config.last_backup_message = "No data to backup" if config.schedule_enabled: @@ -259,12 +259,12 @@ class GitHubBackupService: # Update log and config log.status = push_result["status"] - log.completed_at = datetime.now(UTC) + log.completed_at = datetime.now(timezone.utc) log.commit_sha = push_result.get("commit_sha") log.files_changed = push_result.get("files_changed", 0) log.error_message = push_result.get("error") - config.last_backup_at = datetime.now(UTC) + config.last_backup_at = datetime.now(timezone.utc) config.last_backup_status = push_result["status"] config.last_backup_message = push_result.get("message", "") config.last_backup_commit_sha = push_result.get("commit_sha") @@ -285,10 +285,10 @@ class GitHubBackupService: except Exception as e: logger.error(f"Backup failed: {e}") log.status = "failed" - log.completed_at = datetime.now(UTC) + log.completed_at = datetime.now(timezone.utc) log.error_message = str(e) - config.last_backup_at = datetime.now(UTC) + config.last_backup_at = datetime.now(timezone.utc) config.last_backup_status = "failed" config.last_backup_message = str(e) @@ -572,7 +572,7 @@ class GitHubBackupService: new_tree_sha = tree_response.json()["sha"] # Create commit - commit_message = f"Bambuddy backup - {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}" + commit_message = f"Bambuddy backup - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}" commit_response = await client.post( f"https://api.github.com/repos/{owner}/{repo}/git/commits", headers=headers, @@ -689,7 +689,7 @@ class GitHubBackupService: f"https://api.github.com/repos/{owner}/{repo}/git/commits", headers=headers, json={ - "message": f"Initial Bambuddy backup - {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}", + "message": f"Initial Bambuddy backup - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}", "tree": tree_sha, }, ) diff --git a/backend/app/services/spoolman.py b/backend/app/services/spoolman.py index bf95ae8ca..21f8ed3f0 100644 --- a/backend/app/services/spoolman.py +++ b/backend/app/services/spoolman.py @@ -2,7 +2,7 @@ import logging from dataclasses import dataclass -from datetime import UTC, datetime +from datetime import datetime, timezone import httpx @@ -355,7 +355,7 @@ class SpoolmanClient: data["extra"] = extra # Always update last_used - data["last_used"] = datetime.now(UTC).isoformat() + data["last_used"] = datetime.now(timezone.utc).isoformat() client = await self._get_client() response = await client.patch(f"{self.api_url}/spool/{spool_id}", json=data) diff --git a/backend/app/services/virtual_printer/certificate.py b/backend/app/services/virtual_printer/certificate.py index 5b1029a97..f8b8ad76f 100644 --- a/backend/app/services/virtual_printer/certificate.py +++ b/backend/app/services/virtual_printer/certificate.py @@ -10,7 +10,7 @@ This allows users to add the CA to their slicer's trust store once. import logging import socket -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone from ipaddress import IPv4Address from pathlib import Path @@ -89,7 +89,7 @@ class CertificateService: ca_cert = x509.load_pem_x509_certificate(ca_cert_pem) # Check if CA is expired or about to expire - now = datetime.now(UTC) + now = datetime.now(timezone.utc) days_remaining = (ca_cert.not_valid_after_utc - now).days if days_remaining < CA_EXPIRY_THRESHOLD_DAYS: logger.warning(f"CA certificate expires in {days_remaining} days, will regenerate") @@ -160,7 +160,7 @@ class CertificateService: ] ) - now = datetime.now(UTC) + now = datetime.now(timezone.utc) ca_cert = ( x509.CertificateBuilder() @@ -227,7 +227,7 @@ class CertificateService: # Issuer is the CA issuer = ca_cert.subject - now = datetime.now(UTC) + now = datetime.now(timezone.utc) local_ip = _get_local_ip() logger.info(f"Generating printer certificate with CN={self.serial}, local IP: {local_ip}") diff --git a/backend/app/services/virtual_printer/manager.py b/backend/app/services/virtual_printer/manager.py index 40ef4e152..9f3c27b98 100644 --- a/backend/app/services/virtual_printer/manager.py +++ b/backend/app/services/virtual_printer/manager.py @@ -10,7 +10,7 @@ Supports multiple modes: import asyncio import logging from collections.abc import Callable -from datetime import UTC, datetime +from datetime import datetime, timezone from pathlib import Path from backend.app.core.config import settings as app_settings @@ -598,7 +598,7 @@ class VirtualPrinterManager: file_size=file_path.stat().st_size, source_ip=source_ip, status="pending", - uploaded_at=datetime.now(UTC), + uploaded_at=datetime.now(timezone.utc), ) db.add(pending) await db.commit() diff --git a/pyproject.toml b/pyproject.toml index 844c1dab6..38133daa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ ignore = [ "SIM108", # ternary operator (readability preference) "SIM102", # nested if (readability preference) "SIM105", # contextlib.suppress (readability preference) + "UP017", # datetime.UTC alias (Python 3.11+ only, we support 3.10) ] # Allow autofix for all enabled rules