fix: replace datetime.utcnow() with utcnow()/utcnow_iso() across all source files

Migrates 15 source files from the deprecated datetime.utcnow() API.
Uses utcnow() for in-memory math and utcnow_iso() (strftime format)
for SQLite TEXT column writes to preserve lexicographic sort order.
Also fixes datetime.utcfromtimestamp() in seo/_bing.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-24 10:22:42 +01:00
parent f76d2889e5
commit 5644a1ebf8
18 changed files with 116 additions and 113 deletions

View File

@@ -29,6 +29,8 @@ from ..core import (
fetch_one,
send_email,
slugify,
utcnow,
utcnow_iso,
)
# Blueprint with its own template folder
@@ -64,9 +66,9 @@ def _admin_context():
async def get_dashboard_stats() -> dict:
"""Get admin dashboard statistics."""
now = datetime.utcnow()
now = utcnow()
today = now.date().isoformat()
week_ago = (now - timedelta(days=7)).isoformat()
week_ago = (now - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
users_total = await fetch_one("SELECT COUNT(*) as count FROM users WHERE deleted_at IS NULL")
users_today = await fetch_one(
"SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND deleted_at IS NULL",
@@ -211,7 +213,7 @@ async def retry_task(task_id: int) -> bool:
SET status = 'pending', run_at = ?, error = NULL
WHERE id = ? AND status = 'failed'
""",
(datetime.utcnow().isoformat(), task_id)
(utcnow_iso(), task_id)
)
return result > 0
@@ -522,7 +524,7 @@ async def lead_new():
from ..credits import HEAT_CREDIT_COSTS
credit_cost = HEAT_CREDIT_COSTS.get(heat_score, 8)
now = datetime.utcnow().isoformat()
now = utcnow_iso()
verified_at = now if status != "pending_verification" else None
lead_id = await execute(
@@ -567,7 +569,7 @@ async def lead_forward(lead_id: int):
await flash("Already forwarded to this supplier.", "warning")
return redirect(url_for("admin.lead_detail", lead_id=lead_id))
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"""INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, status, created_at)
VALUES (?, ?, 0, 'sent', ?)""",
@@ -771,7 +773,7 @@ async def supplier_new():
instagram_url = form.get("instagram_url", "").strip()
youtube_url = form.get("youtube_url", "").strip()
now = datetime.utcnow().isoformat()
now = utcnow_iso()
supplier_id = await execute(
"""INSERT INTO suppliers
(name, slug, country_code, city, region, website, description, category,
@@ -865,7 +867,7 @@ async def flag_toggle():
return redirect(url_for("admin.flags"))
new_enabled = 0 if row["enabled"] else 1
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"UPDATE feature_flags SET enabled = ?, updated_at = ? WHERE name = ?",
(new_enabled, now, flag_name),
@@ -940,7 +942,7 @@ async def get_email_stats() -> dict:
total = await fetch_one("SELECT COUNT(*) as cnt FROM email_log")
delivered = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE last_event = 'delivered'")
bounced = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE last_event = 'bounced'")
today = datetime.utcnow().date().isoformat()
today = utcnow().date().isoformat()
sent_today = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE created_at >= ?", (today,))
return {
"total": total["cnt"] if total else 0,
@@ -1487,7 +1489,7 @@ async def scenario_edit(scenario_id: int):
dbl = state.get("dblCourts", 0)
sgl = state.get("sglCourts", 0)
court_config = f"{dbl} double + {sgl} single"
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"""UPDATE published_scenarios
@@ -1740,7 +1742,7 @@ async def article_new():
md_dir.mkdir(parents=True, exist_ok=True)
(md_dir / f"{article_slug}.md").write_text(body)
pub_dt = published_at or datetime.utcnow().isoformat()
pub_dt = published_at or utcnow_iso()
await execute(
"""INSERT INTO articles
@@ -1800,7 +1802,7 @@ async def article_edit(article_id: int):
md_dir.mkdir(parents=True, exist_ok=True)
(md_dir / f"{article['slug']}.md").write_text(body)
now = datetime.utcnow().isoformat()
now = utcnow_iso()
pub_dt = published_at or article["published_at"]
await execute(
@@ -1867,7 +1869,7 @@ async def article_publish(article_id: int):
return redirect(url_for("admin.articles"))
new_status = "published" if article["status"] == "draft" else "draft"
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"UPDATE articles SET status = ?, updated_at = ? WHERE id = ?",
(new_status, now, article_id),

View File

@@ -208,7 +208,7 @@ def create_app() -> Quart:
@app.context_processor
def inject_globals():
from datetime import datetime
from .core import utcnow as _utcnow
lang = g.get("lang") or _detect_lang()
g.lang = lang # ensure g.lang is always set (e.g. for dashboard/billing routes)
effective_lang = lang if lang in SUPPORTED_LANGS else "en"
@@ -217,7 +217,7 @@ def create_app() -> Quart:
"user": g.get("user"),
"subscription": g.get("subscription"),
"is_admin": "admin" in (g.get("user") or {}).get("roles", []),
"now": datetime.utcnow(),
"now": _utcnow(),
"csrf_token": get_csrf_token,
"ab_variant": getattr(g, "ab_variant", None),
"ab_tag": getattr(g, "ab_tag", None),

View File

@@ -18,6 +18,8 @@ from ..core import (
fetch_one,
is_disposable_email,
is_flag_enabled,
utcnow,
utcnow_iso,
)
from ..i18n import SUPPORTED_LANGS, get_translations
@@ -64,7 +66,7 @@ async def get_user_by_email(email: str) -> dict | None:
async def create_user(email: str) -> int:
"""Create new user, return ID."""
now = datetime.utcnow().isoformat()
now = utcnow_iso()
return await execute(
"INSERT INTO users (email, created_at) VALUES (?, ?)", (email.lower(), now)
)
@@ -82,10 +84,10 @@ async def update_user(user_id: int, **fields) -> None:
async def create_auth_token(user_id: int, token: str, minutes: int = None) -> int:
"""Create auth token for user."""
minutes = minutes or config.MAGIC_LINK_EXPIRY_MINUTES
expires = datetime.utcnow() + timedelta(minutes=minutes)
expires = utcnow() + timedelta(minutes=minutes)
return await execute(
"INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (?, ?, ?)",
(user_id, token, expires.isoformat()),
(user_id, token, expires.strftime("%Y-%m-%dT%H:%M:%S")),
)
@@ -98,14 +100,14 @@ async def get_valid_token(token: str) -> dict | None:
JOIN users u ON u.id = at.user_id
WHERE at.token = ? AND at.expires_at > ? AND at.used_at IS NULL
""",
(token, datetime.utcnow().isoformat()),
(token, utcnow_iso()),
)
async def mark_token_used(token_id: int) -> None:
"""Mark token as used."""
await execute(
"UPDATE auth_tokens SET used_at = ? WHERE id = ?", (datetime.utcnow().isoformat(), token_id)
"UPDATE auth_tokens SET used_at = ? WHERE id = ?", (utcnow_iso(), token_id)
)
@@ -331,7 +333,7 @@ async def verify():
await mark_token_used(token_data["id"])
# Update last login
await update_user(token_data["user_id"], last_login_at=datetime.utcnow().isoformat())
await update_user(token_data["user_id"], last_login_at=utcnow_iso())
# Set session
session.permanent = True

View File

@@ -5,7 +5,7 @@ Payment provider: paddle
import json
import secrets
from datetime import datetime
from datetime import datetime, timedelta
from pathlib import Path
from paddle_billing import Client as PaddleClient
@@ -14,7 +14,7 @@ from paddle_billing.Notifications import Secret, Verifier
from quart import Blueprint, flash, g, jsonify, redirect, render_template, request, session, url_for
from ..auth.routes import login_required
from ..core import config, execute, fetch_one, get_paddle_price
from ..core import config, execute, fetch_one, get_paddle_price, utcnow, utcnow_iso
from ..i18n import get_translations
@@ -69,7 +69,7 @@ async def upsert_subscription(
current_period_end: str = None,
) -> int:
"""Create or update subscription. Finds existing by provider_subscription_id."""
now = datetime.utcnow().isoformat()
now = utcnow_iso()
existing = await fetch_one(
"SELECT id FROM subscriptions WHERE provider_subscription_id = ?",
@@ -104,7 +104,7 @@ async def get_subscription_by_provider_id(subscription_id: str) -> dict | None:
async def update_subscription_status(provider_subscription_id: str, status: str, **extra) -> None:
"""Update subscription status by provider subscription ID."""
extra["updated_at"] = datetime.utcnow().isoformat()
extra["updated_at"] = utcnow_iso()
extra["status"] = status
sets = ", ".join(f"{k} = ?" for k in extra)
values = list(extra.values())
@@ -343,7 +343,7 @@ async def _handle_supplier_subscription_activated(data: dict, custom_data: dict)
base_plan, tier = _derive_tier_from_plan(plan)
monthly_credits = PLAN_MONTHLY_CREDITS.get(base_plan, 0)
now = datetime.utcnow().isoformat()
now = utcnow_iso()
async with db_transaction() as db:
# Update supplier record — Basic tier also gets is_verified = 1
@@ -392,7 +392,7 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
"""Handle one-time transaction completion (credit packs, sticky boosts, business plan)."""
supplier_id = custom_data.get("supplier_id")
user_id = custom_data.get("user_id")
now = datetime.utcnow().isoformat()
now = utcnow_iso()
items = data.get("items", [])
for item in items:
@@ -412,10 +412,8 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
# Sticky boost purchases
elif key == "boost_sticky_week" and supplier_id:
from datetime import timedelta
from ..core import transaction as db_transaction
expires = (datetime.utcnow() + timedelta(weeks=1)).isoformat()
expires = (utcnow() + timedelta(weeks=1)).strftime("%Y-%m-%dT%H:%M:%S")
country = custom_data.get("sticky_country", "")
async with db_transaction() as db:
await db.execute(
@@ -430,10 +428,8 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
)
elif key == "boost_sticky_month" and supplier_id:
from datetime import timedelta
from ..core import transaction as db_transaction
expires = (datetime.utcnow() + timedelta(days=30)).isoformat()
expires = (utcnow() + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S")
country = custom_data.get("sticky_country", "")
async with db_transaction() as db:
await db.execute(

View File

@@ -15,7 +15,7 @@ import yaml
from jinja2 import ChainableUndefined, Environment
from ..analytics import fetch_analytics
from ..core import execute, fetch_one, slugify
from ..core import execute, fetch_one, slugify, utcnow_iso
# ── Constants ────────────────────────────────────────────────────────────────
@@ -301,7 +301,7 @@ async def generate_articles(
publish_date = start_date
published_today = 0
generated = 0
now_iso = datetime.now(UTC).isoformat()
now_iso = utcnow_iso()
for row in rows:
for lang in config["languages"]:

View File

@@ -5,9 +5,7 @@ All balance mutations go through this module to keep credit_ledger (source of tr
and suppliers.credit_balance (denormalized cache) in sync within a single transaction.
"""
from datetime import datetime
from .core import execute, fetch_all, fetch_one, transaction
from .core import execute, fetch_all, fetch_one, transaction, utcnow_iso
# Credit cost per heat tier
HEAT_CREDIT_COSTS = {"hot": 35, "warm": 20, "cool": 8}
@@ -44,7 +42,7 @@ async def add_credits(
note: str = None,
) -> int:
"""Add credits to a supplier. Returns new balance."""
now = datetime.utcnow().isoformat()
now = utcnow_iso()
async with transaction() as db:
row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
@@ -73,7 +71,7 @@ async def spend_credits(
note: str = None,
) -> int:
"""Spend credits from a supplier. Returns new balance. Raises InsufficientCredits."""
now = datetime.utcnow().isoformat()
now = utcnow_iso()
async with transaction() as db:
row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
@@ -116,7 +114,7 @@ async def unlock_lead(supplier_id: int, lead_id: int) -> dict:
raise ValueError("Lead not found")
cost = lead["credit_cost"] or compute_credit_cost(lead)
now = datetime.utcnow().isoformat()
now = utcnow_iso()
async with transaction() as db:
# Check balance
@@ -180,7 +178,7 @@ async def monthly_credit_refill(supplier_id: int) -> int:
if not row or not row["monthly_credits"]:
return 0
now = datetime.utcnow().isoformat()
now = utcnow_iso()
new_balance = await add_credits(
supplier_id,
row["monthly_credits"],

View File

@@ -1,13 +1,12 @@
"""
Dashboard domain: user dashboard and settings.
"""
from datetime import datetime
from pathlib import Path
from quart import Blueprint, flash, g, redirect, render_template, request, url_for
from ..auth.routes import login_required, update_user
from ..core import csrf_protect, fetch_one, soft_delete
from ..core import csrf_protect, fetch_one, soft_delete, utcnow_iso
from ..i18n import get_translations
bp = Blueprint(
@@ -57,7 +56,7 @@ async def settings():
await update_user(
g.user["id"],
name=form.get("name", "").strip() or None,
updated_at=datetime.utcnow().isoformat(),
updated_at=utcnow_iso(),
)
t = get_translations(g.get("lang") or "en")
await flash(t["dash_settings_saved"], "success")

View File

@@ -2,12 +2,11 @@
Supplier directory: public, searchable listing of padel court suppliers.
"""
from datetime import UTC, datetime
from pathlib import Path
from quart import Blueprint, g, make_response, redirect, render_template, request, url_for
from ..core import csrf_protect, execute, fetch_all, fetch_one
from ..core import csrf_protect, execute, fetch_all, fetch_one, utcnow_iso
from ..i18n import get_translations
bp = Blueprint(
@@ -89,7 +88,7 @@ async def _build_directory_query(q, country, category, region, page, per_page=24
lang = g.get("lang", "en")
cat_labels, country_labels, region_labels = get_directory_labels(lang)
now = datetime.now(UTC).isoformat()
now = utcnow_iso()
params: list = []
wheres: list[str] = []

View File

@@ -27,6 +27,7 @@ from ..core import (
is_disposable_email,
is_plausible_phone,
send_email,
utcnow_iso,
)
from ..i18n import get_translations
@@ -102,7 +103,7 @@ async def suppliers():
form.get("court_count", 0),
form.get("budget", 0),
form.get("message", ""),
datetime.utcnow().isoformat(),
utcnow_iso(),
),
)
# Notify admin
@@ -147,7 +148,7 @@ async def financing():
form.get("court_count", 0),
form.get("budget", 0),
form.get("message", ""),
datetime.utcnow().isoformat(),
utcnow_iso(),
),
)
await send_email(
@@ -375,7 +376,7 @@ async def quote_request():
status,
credit_cost,
secrets.token_urlsafe(16),
datetime.utcnow().isoformat(),
utcnow_iso(),
),
)
@@ -520,7 +521,7 @@ async def verify_quote():
from ..credits import compute_credit_cost
credit_cost = compute_credit_cost(dict(lead))
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"UPDATE lead_requests SET status = 'new', verified_at = ?, credit_cost = ? WHERE id = ?",
(now, credit_cost, lead["id"]),

View File

@@ -4,7 +4,6 @@ Planner domain: padel court financial planner + scenario management.
import json
import math
from datetime import datetime
from pathlib import Path
from quart import Blueprint, Response, g, jsonify, render_template, request
@@ -18,6 +17,7 @@ from ..core import (
fetch_all,
fetch_one,
get_paddle_price,
utcnow_iso,
)
from ..i18n import get_translations
from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
@@ -502,7 +502,7 @@ async def save_scenario():
location = form.get("location", "")
scenario_id = form.get("scenario_id")
now = datetime.utcnow().isoformat()
now = utcnow_iso()
is_first_save = not scenario_id and (await count_scenarios(g.user["id"])) == 0
@@ -563,7 +563,7 @@ async def get_scenario(scenario_id: int):
@login_required
@csrf_protect
async def delete_scenario(scenario_id: int):
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"UPDATE scenarios SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL",
(now, scenario_id, g.user["id"]),

View File

@@ -18,7 +18,7 @@ import json
import os
import sqlite3
import sys
from datetime import date, timedelta
from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
@@ -1390,7 +1390,7 @@ def seed_templates(conn: sqlite3.Connection) -> dict[str, int]:
def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> int:
"""Insert template_data rows for all cities × languages. Returns count inserted."""
now = __import__("datetime").datetime.utcnow().isoformat()
now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
inserted = 0
en_id = template_ids.get("city-padel-cost-en")

View File

@@ -10,7 +10,7 @@ Usage:
import os
import sqlite3
import sys
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
@@ -292,7 +292,7 @@ def main():
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
now = datetime.utcnow()
now = datetime.now(UTC)
# 1. Create dev user
print("Creating dev user (dev@localhost)...")
@@ -303,7 +303,7 @@ def main():
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
("dev@localhost", "Dev User", now.isoformat()),
("dev@localhost", "Dev User", now.strftime("%Y-%m-%dT%H:%M:%S")),
)
dev_user_id = cursor.lastrowid
print(f" Created (id={dev_user_id})")
@@ -336,7 +336,7 @@ def main():
s["website"], s["description"], s["category"], s["tier"],
s["credit_balance"], s["monthly_credits"], s["contact_name"],
s["contact_email"], s["years_in_business"], s["project_count"],
s["service_area"], now.isoformat(),
s["service_area"], now.strftime("%Y-%m-%dT%H:%M:%S"),
),
)
supplier_ids[s["slug"]] = cursor.lastrowid
@@ -349,7 +349,7 @@ def main():
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"),
]
period_end = (now + timedelta(days=30)).isoformat()
period_end = (now + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S")
for slug, plan, email, name in claimed_suppliers:
sid = supplier_ids.get(slug)
if not sid:
@@ -364,14 +364,14 @@ def main():
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
(email, name, now.isoformat()),
(email, name, now.strftime("%Y-%m-%dT%H:%M:%S")),
)
owner_id = cursor.lastrowid
# Claim the supplier
conn.execute(
"UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL",
(owner_id, now.isoformat(), sid),
(owner_id, now.strftime("%Y-%m-%dT%H:%M:%S"), sid),
)
# Create billing customer record
@@ -382,7 +382,7 @@ def main():
conn.execute(
"""INSERT INTO billing_customers (user_id, provider_customer_id, created_at)
VALUES (?, ?, ?)""",
(owner_id, f"ctm_dev_{slug}", now.isoformat()),
(owner_id, f"ctm_dev_{slug}", now.strftime("%Y-%m-%dT%H:%M:%S")),
)
# Create active subscription
@@ -396,7 +396,7 @@ def main():
current_period_end, created_at)
VALUES (?, ?, 'active', ?, ?, ?)""",
(owner_id, plan, f"sub_dev_{slug}",
period_end, now.isoformat()),
period_end, now.strftime("%Y-%m-%dT%H:%M:%S")),
)
print(f" {slug} -> owner {email} ({plan})")

View File

@@ -3,12 +3,12 @@
Uses an API key for auth. Fetches query stats and page stats.
"""
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta
from urllib.parse import urlparse
import httpx
from ..core import config, execute
from ..core import config, execute, utcnow, utcnow_iso
_TIMEOUT_SECONDS = 30
@@ -27,7 +27,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
if not config.BING_WEBMASTER_API_KEY or not config.BING_SITE_URL:
return 0 # Bing not configured — skip silently
started_at = datetime.utcnow()
started_at = utcnow()
try:
rows_synced = 0
@@ -48,14 +48,14 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
if not isinstance(entries, list):
entries = []
cutoff = datetime.utcnow() - timedelta(days=days_back)
cutoff = utcnow() - timedelta(days=days_back)
for entry in entries:
# Bing date format: "/Date(1708905600000)/" (ms since epoch)
date_str = entry.get("Date", "")
if "/Date(" in date_str:
ms = int(date_str.split("(")[1].split(")")[0])
entry_date = datetime.utcfromtimestamp(ms / 1000)
entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
else:
continue
@@ -99,7 +99,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
date_str = entry.get("Date", "")
if "/Date(" in date_str:
ms = int(date_str.split("(")[1].split(")")[0])
entry_date = datetime.utcfromtimestamp(ms / 1000)
entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
else:
continue
@@ -122,21 +122,21 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
)
rows_synced += 1
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('bing', 'success', ?, ?, ?, ?)""",
(rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
(rows_synced, started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
)
return rows_synced
except Exception as exc:
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('bing', 'failed', 0, ?, ?, ?, ?)""",
(str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
(str(exc), started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
)
raise

View File

@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import urlparse
from ..core import config, execute
from ..core import config, execute, utcnow, utcnow_iso
# GSC returns max 25K rows per request
_ROWS_PER_PAGE = 25_000
@@ -95,11 +95,11 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
if not config.GSC_SERVICE_ACCOUNT_PATH or not config.GSC_SITE_URL:
return 0 # GSC not configured — skip silently
started_at = datetime.utcnow()
started_at = utcnow()
# GSC has ~2 day delay; fetch from days_back ago to 2 days ago
end_date = (datetime.utcnow() - timedelta(days=2)).strftime("%Y-%m-%d")
start_date = (datetime.utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
end_date = (utcnow() - timedelta(days=2)).strftime("%Y-%m-%d")
start_date = (utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
try:
rows = await asyncio.to_thread(
@@ -122,21 +122,21 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
)
rows_synced += 1
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('gsc', 'success', ?, ?, ?, ?)""",
(rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
(rows_synced, started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
)
return rows_synced
except Exception as exc:
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('gsc', 'failed', 0, ?, ?, ?, ?)""",
(str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
(str(exc), started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
)
raise

View File

@@ -4,14 +4,14 @@ All heavy lifting happens in SQL. Functions accept filter parameters
and return plain dicts/lists.
"""
from datetime import datetime, timedelta
from datetime import timedelta
from ..core import execute, fetch_all, fetch_one
from ..core import execute, fetch_all, fetch_one, utcnow
def _date_cutoff(date_range_days: int) -> str:
"""Return ISO date string for N days ago."""
return (datetime.utcnow() - timedelta(days=date_range_days)).strftime("%Y-%m-%d")
return (utcnow() - timedelta(days=date_range_days)).strftime("%Y-%m-%d")
async def get_search_performance(

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
import httpx
from ..core import config, execute
from ..core import config, execute, utcnow, utcnow_iso
_TIMEOUT_SECONDS = 15
@@ -21,7 +21,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
if not config.UMAMI_API_TOKEN or not config.UMAMI_API_URL:
return 0 # Umami not configured — skip silently
started_at = datetime.utcnow()
started_at = utcnow()
try:
rows_synced = 0
@@ -34,7 +34,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
# (Umami's metrics endpoint returns totals for the period,
# so we query one day at a time for daily granularity)
for day_offset in range(days_back):
day = datetime.utcnow() - timedelta(days=day_offset + 1)
day = utcnow() - timedelta(days=day_offset + 1)
metric_date = day.strftime("%Y-%m-%d")
start_ms = int(day.replace(hour=0, minute=0, second=0).timestamp() * 1000)
end_ms = int(day.replace(hour=23, minute=59, second=59).timestamp() * 1000)
@@ -96,21 +96,21 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
(metric_date, page_count, visitors, br, avg_time),
)
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, started_at, completed_at, duration_ms)
VALUES ('umami', 'success', ?, ?, ?, ?)""",
(rows_synced, started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
(rows_synced, started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
)
return rows_synced
except Exception as exc:
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
await execute(
"""INSERT INTO seo_sync_log
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
VALUES ('umami', 'failed', 0, ?, ?, ?, ?)""",
(str(exc), started_at.isoformat(), datetime.utcnow().isoformat(), duration_ms),
(str(exc), started_at.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
)
raise

View File

@@ -5,12 +5,10 @@ NOT behind @role_required: Resend posts here unauthenticated.
Verification uses RESEND_WEBHOOK_SECRET via the Resend SDK.
"""
from datetime import datetime
import resend
from quart import Blueprint, jsonify, request
from .core import config, execute
from .core import config, execute, utcnow_iso
bp = Blueprint("webhooks", __name__, url_prefix="/webhooks")
@@ -67,7 +65,7 @@ async def _handle_delivery_event(event_type: str, data: dict) -> None:
return
last_event, ts_col = _EVENT_UPDATES[event_type]
now = datetime.utcnow().isoformat()
now = utcnow_iso()
if ts_col:
await execute(
@@ -87,7 +85,7 @@ async def _handle_inbound(data: dict) -> None:
if not resend_id:
return
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"""INSERT OR IGNORE INTO inbound_emails
(resend_id, message_id, in_reply_to, from_addr, to_addr,

View File

@@ -7,7 +7,17 @@ import json
import traceback
from datetime import datetime, timedelta
from .core import EMAIL_ADDRESSES, config, execute, fetch_all, fetch_one, init_db, send_email
from .core import (
EMAIL_ADDRESSES,
config,
execute,
fetch_all,
fetch_one,
init_db,
send_email,
utcnow,
utcnow_iso,
)
from .i18n import get_translations
# Task handlers registry
@@ -29,7 +39,7 @@ def _email_wrap(body: str, lang: str = "en", preheader: str = "") -> str:
preheader: hidden preview text shown in email client list views.
"""
year = datetime.utcnow().year
year = utcnow().year
tagline = _t("email_footer_tagline", lang)
copyright_text = _t("email_footer_copyright", lang, year=year, app_name=config.APP_NAME)
# Hidden preheader trick: visible text + invisible padding to prevent
@@ -132,15 +142,15 @@ async def enqueue(task_name: str, payload: dict = None, run_at: datetime = None)
(
task_name,
json.dumps(payload or {}),
(run_at or datetime.utcnow()).isoformat(),
datetime.utcnow().isoformat(),
(run_at or utcnow()).strftime("%Y-%m-%dT%H:%M:%S"),
utcnow_iso(),
),
)
async def get_pending_tasks(limit: int = 10) -> list[dict]:
"""Get pending tasks ready to run."""
now = datetime.utcnow().isoformat()
now = utcnow_iso()
return await fetch_all(
"""
SELECT * FROM tasks
@@ -156,7 +166,7 @@ async def mark_complete(task_id: int) -> None:
"""Mark task as completed."""
await execute(
"UPDATE tasks SET status = 'complete', completed_at = ? WHERE id = ?",
(datetime.utcnow().isoformat(), task_id),
(utcnow_iso(), task_id),
)
@@ -167,7 +177,7 @@ async def mark_failed(task_id: int, error: str, retries: int) -> None:
if retries < max_retries:
# Exponential backoff: 1min, 5min, 25min
delay = timedelta(minutes=5**retries)
run_at = datetime.utcnow() + delay
run_at = utcnow() + delay
await execute(
"""
@@ -385,13 +395,13 @@ async def handle_send_waitlist_confirmation(payload: dict) -> None:
@task("cleanup_expired_tokens")
async def handle_cleanup_tokens(payload: dict) -> None:
"""Clean up expired auth tokens."""
await execute("DELETE FROM auth_tokens WHERE expires_at < ?", (datetime.utcnow().isoformat(),))
await execute("DELETE FROM auth_tokens WHERE expires_at < ?", (utcnow_iso(),))
@task("cleanup_rate_limits")
async def handle_cleanup_rate_limits(payload: dict) -> None:
"""Clean up old rate limit entries."""
cutoff = (datetime.utcnow() - timedelta(hours=1)).isoformat()
cutoff = (utcnow() - timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%S")
await execute("DELETE FROM rate_limits WHERE timestamp < ?", (cutoff,))
@@ -497,7 +507,7 @@ async def handle_send_lead_forward_email(payload: dict) -> None:
)
# Update email_sent_at on lead_forward
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"UPDATE lead_forwards SET email_sent_at = ? WHERE lead_id = ? AND supplier_id = ?",
(now, lead_id, supplier_id),
@@ -621,7 +631,7 @@ async def handle_generate_business_plan(payload: dict) -> None:
file_path.write_bytes(pdf_bytes)
# Update record
now = datetime.utcnow().isoformat()
now = utcnow_iso()
await execute(
"UPDATE business_plan_exports SET status = 'ready', file_path = ?, completed_at = ? WHERE id = ?",
(str(file_path), now, export_id),
@@ -664,7 +674,7 @@ async def handle_generate_business_plan(payload: dict) -> None:
@task("cleanup_old_tasks")
async def handle_cleanup_tasks(payload: dict) -> None:
"""Clean up completed/failed tasks older than 7 days."""
cutoff = (datetime.utcnow() - timedelta(days=7)).isoformat()
cutoff = (utcnow() - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
await execute(
"DELETE FROM tasks WHERE status IN ('complete', 'failed') AND created_at < ?", (cutoff,)
)
@@ -791,9 +801,7 @@ async def run_scheduler() -> None:
await enqueue("cleanup_old_tasks")
# Monthly credit refill — run on the 1st of each month
from datetime import datetime
today = datetime.utcnow()
today = utcnow()
this_month = f"{today.year}-{today.month:02d}"
if today.day == 1 and last_credit_refill != this_month:
await enqueue("refill_monthly_credits")