Browse Source

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
maziggy 3 months ago
parent
commit
819ab896bd

+ 3 - 3
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

+ 2 - 2
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}), "

+ 12 - 12
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,
                 },
             )

+ 2 - 2
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)

+ 4 - 4
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}")
 

+ 2 - 2
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()

+ 1 - 0
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