From 851defa8597c4d6e70b290cefe74b2affddb75e0 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 14 May 2026 12:07:00 -0500 Subject: [PATCH] Replace deprecated datetime.utcnow() with datetime.now(UTC) throughout Python 3.12 deprecates datetime.utcnow() with a DeprecationWarning and it will be removed in a future release. Replace all call-site usages in services, routes, and utils with datetime.now(UTC), and replace the SQLAlchemy ORM column defaults (which used the bare function reference datetime.utcnow) with lambda: datetime.now(UTC) so that the returned objects are timezone-aware and consistent with Python best practice. Affected: database/models.py, services/{stories,history,profiles, channels,export_import}.py, routes/{profiles,tasks}.py, utils/tasks.py --- backend/database/models.py | 34 +++++++++++++++---------------- backend/routes/profiles.py | 4 ++-- backend/routes/tasks.py | 6 +++--- backend/services/channels.py | 4 ++-- backend/services/export_import.py | 2 +- backend/services/history.py | 4 ++-- backend/services/profiles.py | 14 ++++++------- backend/services/stories.py | 34 +++++++++++++++---------------- backend/utils/tasks.py | 6 +++--- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/backend/database/models.py b/backend/database/models.py index b85a55b1..5ced0c15 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -1,6 +1,6 @@ """ORM model definitions for the voicebox SQLite database.""" -from datetime import datetime +from datetime import UTC, datetime import uuid from sqlalchemy import Column, String, Integer, Float, DateTime, Text, ForeignKey, Boolean, JSON @@ -44,8 +44,8 @@ class VoiceProfile(Base): # cloning metadata above). personality = Column(Text, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) class ProfileSample(Base): @@ -82,7 +82,7 @@ class Generation(Base): # profile's personality LLM before TTS. Future sources (bulk import, # agent replies, etc.) can extend this. source = Column(String, nullable=False, default="manual") - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) class Story(Base): @@ -93,8 +93,8 @@ class Story(Base): id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String, nullable=False) description = Column(Text) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) class StoryItem(Base): @@ -111,7 +111,7 @@ class StoryItem(Base): trim_start_ms = Column(Integer, nullable=False, default=0) trim_end_ms = Column(Integer, nullable=False, default=0) volume = Column(Float, nullable=False, default=1.0) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) class Project(Base): @@ -122,8 +122,8 @@ class Project(Base): id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String, nullable=False) data = Column(Text) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) class GenerationVersion(Base): @@ -138,7 +138,7 @@ class GenerationVersion(Base): effects_chain = Column(Text, nullable=True) source_version_id = Column(String, ForeignKey("generation_versions.id"), nullable=True) is_default = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) class EffectPreset(Base): @@ -152,7 +152,7 @@ class EffectPreset(Base): effects_chain = Column(Text, nullable=False) is_builtin = Column(Boolean, default=False) sort_order = Column(Integer, default=100) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) class AudioChannel(Base): @@ -163,7 +163,7 @@ class AudioChannel(Base): id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String, nullable=False) is_default = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) class ChannelDeviceMapping(Base): @@ -218,7 +218,7 @@ class CaptureSettings(Base): chord_toggle_to_talk_keys = Column( JSON, nullable=False, default=default_toggle_to_talk_chord ) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) class GenerationSettings(Base): @@ -231,7 +231,7 @@ class GenerationSettings(Base): crossfade_ms = Column(Integer, nullable=False, default=50) normalize_audio = Column(Boolean, nullable=False, default=True) autoplay_on_generate = Column(Boolean, nullable=False, default=True) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) class CloudSettings(Base): @@ -276,8 +276,8 @@ class MCPClientBinding(Base): # (rewrite) before TTS by default. Callers can still override per call. default_personality = Column(Boolean, nullable=False, default=False) last_seen_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) class Capture(Base): @@ -300,4 +300,4 @@ class Capture(Base): stt_model = Column(String, nullable=True) llm_model = Column(String, nullable=True) refinement_flags = Column(Text, nullable=True) # JSON blob - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) diff --git a/backend/routes/profiles.py b/backend/routes/profiles.py index 68e5f2af..3dd667df 100644 --- a/backend/routes/profiles.py +++ b/backend/routes/profiles.py @@ -4,7 +4,7 @@ import io import json as _json import logging import tempfile -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile @@ -356,7 +356,7 @@ async def update_profile_effects( else: profile.effects_chain = None - profile.updated_at = datetime.utcnow() + profile.updated_at = datetime.now(UTC) db.commit() db.refresh(profile) diff --git a/backend/routes/tasks.py b/backend/routes/tasks.py index c3fc5a91..4b7f7f40 100644 --- a/backend/routes/tasks.py +++ b/backend/routes/tasks.py @@ -1,6 +1,6 @@ """Task and cache management endpoints.""" -from datetime import datetime +from datetime import UTC, datetime from fastapi import APIRouter @@ -91,9 +91,9 @@ async def get_active_tasks(): try: started_at = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) except (ValueError, AttributeError): - started_at = datetime.utcnow() + started_at = datetime.now(UTC) else: - started_at = datetime.utcnow() + started_at = datetime.now(UTC) active_downloads.append( models.ActiveDownloadTask( diff --git a/backend/services/channels.py b/backend/services/channels.py index f7d9d788..3ca1928b 100644 --- a/backend/services/channels.py +++ b/backend/services/channels.py @@ -3,7 +3,7 @@ Audio channel management module. """ from typing import List, Optional -from datetime import datetime +from datetime import UTC, datetime import uuid from sqlalchemy.orm import Session @@ -81,7 +81,7 @@ async def create_channel( id=str(uuid.uuid4()), name=data.name, is_default=False, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(channel) db.flush() diff --git a/backend/services/export_import.py b/backend/services/export_import.py index 514eaacd..4ff280ce 100644 --- a/backend/services/export_import.py +++ b/backend/services/export_import.py @@ -432,7 +432,7 @@ async def import_generation_from_zip(file_bytes: bytes, db: Session) -> dict: duration=generation_data["duration"], seed=generation_data.get("seed"), instruct=generation_data.get("instruct"), - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(db_generation) diff --git a/backend/services/history.py b/backend/services/history.py index 3062f7d6..d79ec83d 100644 --- a/backend/services/history.py +++ b/backend/services/history.py @@ -3,7 +3,7 @@ Generation history management module. """ from typing import List, Optional, Tuple -from datetime import datetime +from datetime import UTC, datetime import uuid import shutil from pathlib import Path @@ -104,7 +104,7 @@ async def create_generation( model_size=model_size, status=status, source=source, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(db_generation) diff --git a/backend/services/profiles.py b/backend/services/profiles.py index d7d32fa0..f9872995 100644 --- a/backend/services/profiles.py +++ b/backend/services/profiles.py @@ -4,7 +4,7 @@ import json as _json import logging import shutil import uuid -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from sqlalchemy import func @@ -183,8 +183,8 @@ async def create_profile( design_prompt=data.design_prompt, default_engine=default_engine, personality=data.personality, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) db.add(db_profile) @@ -244,7 +244,7 @@ async def add_profile_sample( db.add(db_sample) - profile.updated_at = datetime.utcnow() + profile.updated_at = datetime.now(UTC) db.commit() db.refresh(db_sample) @@ -403,7 +403,7 @@ async def update_profile( profile.personality = data.personality if data.default_engine is not None: profile.default_engine = data.default_engine or None # empty string → NULL - profile.updated_at = datetime.utcnow() + profile.updated_at = datetime.now(UTC) db.commit() db.refresh(profile) @@ -672,7 +672,7 @@ async def upload_avatar( process_avatar(image_path, str(output_path)) profile.avatar_path = config.to_storage_path(output_path) - profile.updated_at = datetime.utcnow() + profile.updated_at = datetime.now(UTC) db.commit() db.refresh(profile) @@ -703,7 +703,7 @@ async def delete_avatar( avatar_path.unlink() profile.avatar_path = None - profile.updated_at = datetime.utcnow() + profile.updated_at = datetime.now(UTC) db.commit() diff --git a/backend/services/stories.py b/backend/services/stories.py index 44ae1cdc..b1f0905d 100644 --- a/backend/services/stories.py +++ b/backend/services/stories.py @@ -3,7 +3,7 @@ Story management module. """ from typing import List, Optional -from datetime import datetime +from datetime import UTC, datetime import uuid import tempfile from pathlib import Path @@ -96,8 +96,8 @@ async def create_story( id=str(uuid.uuid4()), name=data.name, description=data.description, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) db.add(db_story) @@ -206,7 +206,7 @@ async def update_story( story.name = data.name story.description = data.description - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(story) @@ -314,13 +314,13 @@ async def add_item_to_story( generation_id=data.generation_id, start_time_ms=start_time_ms, track=track, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(item) # Update story updated_at - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(item) @@ -373,7 +373,7 @@ async def move_story_item( # Update story updated_at story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(item) @@ -417,7 +417,7 @@ async def remove_item_from_story( # Update story updated_at story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() return True @@ -470,7 +470,7 @@ async def trim_story_item( # Update story updated_at story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(item) @@ -503,7 +503,7 @@ async def update_story_item_volume( story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(item) @@ -576,7 +576,7 @@ async def split_story_item( trim_start_ms=absolute_split_ms, trim_end_ms=current_trim_end, volume=getattr(item, "volume", 1.0), - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(new_item) @@ -584,7 +584,7 @@ async def split_story_item( # Update story updated_at story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(item) @@ -650,7 +650,7 @@ async def duplicate_story_item( trim_start_ms=current_trim_start, trim_end_ms=current_trim_end, volume=getattr(original_item, "volume", 1.0), - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(new_item) @@ -658,7 +658,7 @@ async def duplicate_story_item( # Update story updated_at story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(new_item) @@ -700,7 +700,7 @@ async def update_story_item_times( item_map[update.generation_id].start_time_ms = update.start_time_ms # Update story updated_at - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() return True @@ -764,7 +764,7 @@ async def reorder_story_items( updated_items.append(_build_item_detail(item, generation, profile_name, db)) # Update story updated_at - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() return updated_items @@ -823,7 +823,7 @@ async def set_story_item_version( # Update story updated_at story = db.query(DBStory).filter_by(id=story_id).first() if story: - story.updated_at = datetime.utcnow() + story.updated_at = datetime.now(UTC) db.commit() db.refresh(item) diff --git a/backend/utils/tasks.py b/backend/utils/tasks.py index efec184e..e266f051 100644 --- a/backend/utils/tasks.py +++ b/backend/utils/tasks.py @@ -3,7 +3,7 @@ Task tracking for active downloads and generations. """ from typing import Optional, Dict, List -from datetime import datetime +from datetime import UTC, datetime from dataclasses import dataclass, field @@ -12,7 +12,7 @@ class DownloadTask: """Represents an active download task.""" model_name: str status: str = "downloading" # downloading, extracting, complete, error - started_at: datetime = field(default_factory=datetime.utcnow) + started_at: datetime = field(default_factory=lambda: datetime.now(UTC)) error: Optional[str] = None @@ -22,7 +22,7 @@ class GenerationTask: task_id: str profile_id: str text_preview: str # First 50 chars of text - started_at: datetime = field(default_factory=datetime.utcnow) + started_at: datetime = field(default_factory=lambda: datetime.now(UTC)) class TaskManager: