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:
@@ -29,6 +29,8 @@ from ..core import (
|
|||||||
fetch_one,
|
fetch_one,
|
||||||
send_email,
|
send_email,
|
||||||
slugify,
|
slugify,
|
||||||
|
utcnow,
|
||||||
|
utcnow_iso,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Blueprint with its own template folder
|
# Blueprint with its own template folder
|
||||||
@@ -64,9 +66,9 @@ def _admin_context():
|
|||||||
|
|
||||||
async def get_dashboard_stats() -> dict:
|
async def get_dashboard_stats() -> dict:
|
||||||
"""Get admin dashboard statistics."""
|
"""Get admin dashboard statistics."""
|
||||||
now = datetime.utcnow()
|
now = utcnow()
|
||||||
today = now.date().isoformat()
|
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_total = await fetch_one("SELECT COUNT(*) as count FROM users WHERE deleted_at IS NULL")
|
||||||
users_today = await fetch_one(
|
users_today = await fetch_one(
|
||||||
"SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND deleted_at IS NULL",
|
"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
|
SET status = 'pending', run_at = ?, error = NULL
|
||||||
WHERE id = ? AND status = 'failed'
|
WHERE id = ? AND status = 'failed'
|
||||||
""",
|
""",
|
||||||
(datetime.utcnow().isoformat(), task_id)
|
(utcnow_iso(), task_id)
|
||||||
)
|
)
|
||||||
return result > 0
|
return result > 0
|
||||||
|
|
||||||
@@ -522,7 +524,7 @@ async def lead_new():
|
|||||||
|
|
||||||
from ..credits import HEAT_CREDIT_COSTS
|
from ..credits import HEAT_CREDIT_COSTS
|
||||||
credit_cost = HEAT_CREDIT_COSTS.get(heat_score, 8)
|
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
|
verified_at = now if status != "pending_verification" else None
|
||||||
|
|
||||||
lead_id = await execute(
|
lead_id = await execute(
|
||||||
@@ -567,7 +569,7 @@ async def lead_forward(lead_id: int):
|
|||||||
await flash("Already forwarded to this supplier.", "warning")
|
await flash("Already forwarded to this supplier.", "warning")
|
||||||
return redirect(url_for("admin.lead_detail", lead_id=lead_id))
|
return redirect(url_for("admin.lead_detail", lead_id=lead_id))
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"""INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, status, created_at)
|
"""INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, status, created_at)
|
||||||
VALUES (?, ?, 0, 'sent', ?)""",
|
VALUES (?, ?, 0, 'sent', ?)""",
|
||||||
@@ -771,7 +773,7 @@ async def supplier_new():
|
|||||||
instagram_url = form.get("instagram_url", "").strip()
|
instagram_url = form.get("instagram_url", "").strip()
|
||||||
youtube_url = form.get("youtube_url", "").strip()
|
youtube_url = form.get("youtube_url", "").strip()
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
supplier_id = await execute(
|
supplier_id = await execute(
|
||||||
"""INSERT INTO suppliers
|
"""INSERT INTO suppliers
|
||||||
(name, slug, country_code, city, region, website, description, category,
|
(name, slug, country_code, city, region, website, description, category,
|
||||||
@@ -865,7 +867,7 @@ async def flag_toggle():
|
|||||||
return redirect(url_for("admin.flags"))
|
return redirect(url_for("admin.flags"))
|
||||||
|
|
||||||
new_enabled = 0 if row["enabled"] else 1
|
new_enabled = 0 if row["enabled"] else 1
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE feature_flags SET enabled = ?, updated_at = ? WHERE name = ?",
|
"UPDATE feature_flags SET enabled = ?, updated_at = ? WHERE name = ?",
|
||||||
(new_enabled, now, flag_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")
|
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'")
|
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'")
|
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,))
|
sent_today = await fetch_one("SELECT COUNT(*) as cnt FROM email_log WHERE created_at >= ?", (today,))
|
||||||
return {
|
return {
|
||||||
"total": total["cnt"] if total else 0,
|
"total": total["cnt"] if total else 0,
|
||||||
@@ -1487,7 +1489,7 @@ async def scenario_edit(scenario_id: int):
|
|||||||
dbl = state.get("dblCourts", 0)
|
dbl = state.get("dblCourts", 0)
|
||||||
sgl = state.get("sglCourts", 0)
|
sgl = state.get("sglCourts", 0)
|
||||||
court_config = f"{dbl} double + {sgl} single"
|
court_config = f"{dbl} double + {sgl} single"
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
await execute(
|
await execute(
|
||||||
"""UPDATE published_scenarios
|
"""UPDATE published_scenarios
|
||||||
@@ -1740,7 +1742,7 @@ async def article_new():
|
|||||||
md_dir.mkdir(parents=True, exist_ok=True)
|
md_dir.mkdir(parents=True, exist_ok=True)
|
||||||
(md_dir / f"{article_slug}.md").write_text(body)
|
(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(
|
await execute(
|
||||||
"""INSERT INTO articles
|
"""INSERT INTO articles
|
||||||
@@ -1800,7 +1802,7 @@ async def article_edit(article_id: int):
|
|||||||
md_dir.mkdir(parents=True, exist_ok=True)
|
md_dir.mkdir(parents=True, exist_ok=True)
|
||||||
(md_dir / f"{article['slug']}.md").write_text(body)
|
(md_dir / f"{article['slug']}.md").write_text(body)
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
pub_dt = published_at or article["published_at"]
|
pub_dt = published_at or article["published_at"]
|
||||||
|
|
||||||
await execute(
|
await execute(
|
||||||
@@ -1867,7 +1869,7 @@ async def article_publish(article_id: int):
|
|||||||
return redirect(url_for("admin.articles"))
|
return redirect(url_for("admin.articles"))
|
||||||
|
|
||||||
new_status = "published" if article["status"] == "draft" else "draft"
|
new_status = "published" if article["status"] == "draft" else "draft"
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE articles SET status = ?, updated_at = ? WHERE id = ?",
|
"UPDATE articles SET status = ?, updated_at = ? WHERE id = ?",
|
||||||
(new_status, now, article_id),
|
(new_status, now, article_id),
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ def create_app() -> Quart:
|
|||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_globals():
|
def inject_globals():
|
||||||
from datetime import datetime
|
from .core import utcnow as _utcnow
|
||||||
lang = g.get("lang") or _detect_lang()
|
lang = g.get("lang") or _detect_lang()
|
||||||
g.lang = lang # ensure g.lang is always set (e.g. for dashboard/billing routes)
|
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"
|
effective_lang = lang if lang in SUPPORTED_LANGS else "en"
|
||||||
@@ -217,7 +217,7 @@ def create_app() -> Quart:
|
|||||||
"user": g.get("user"),
|
"user": g.get("user"),
|
||||||
"subscription": g.get("subscription"),
|
"subscription": g.get("subscription"),
|
||||||
"is_admin": "admin" in (g.get("user") or {}).get("roles", []),
|
"is_admin": "admin" in (g.get("user") or {}).get("roles", []),
|
||||||
"now": datetime.utcnow(),
|
"now": _utcnow(),
|
||||||
"csrf_token": get_csrf_token,
|
"csrf_token": get_csrf_token,
|
||||||
"ab_variant": getattr(g, "ab_variant", None),
|
"ab_variant": getattr(g, "ab_variant", None),
|
||||||
"ab_tag": getattr(g, "ab_tag", None),
|
"ab_tag": getattr(g, "ab_tag", None),
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from ..core import (
|
|||||||
fetch_one,
|
fetch_one,
|
||||||
is_disposable_email,
|
is_disposable_email,
|
||||||
is_flag_enabled,
|
is_flag_enabled,
|
||||||
|
utcnow,
|
||||||
|
utcnow_iso,
|
||||||
)
|
)
|
||||||
from ..i18n import SUPPORTED_LANGS, get_translations
|
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:
|
async def create_user(email: str) -> int:
|
||||||
"""Create new user, return ID."""
|
"""Create new user, return ID."""
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
return await execute(
|
return await execute(
|
||||||
"INSERT INTO users (email, created_at) VALUES (?, ?)", (email.lower(), now)
|
"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:
|
async def create_auth_token(user_id: int, token: str, minutes: int = None) -> int:
|
||||||
"""Create auth token for user."""
|
"""Create auth token for user."""
|
||||||
minutes = minutes or config.MAGIC_LINK_EXPIRY_MINUTES
|
minutes = minutes or config.MAGIC_LINK_EXPIRY_MINUTES
|
||||||
expires = datetime.utcnow() + timedelta(minutes=minutes)
|
expires = utcnow() + timedelta(minutes=minutes)
|
||||||
return await execute(
|
return await execute(
|
||||||
"INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (?, ?, ?)",
|
"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
|
JOIN users u ON u.id = at.user_id
|
||||||
WHERE at.token = ? AND at.expires_at > ? AND at.used_at IS NULL
|
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:
|
async def mark_token_used(token_id: int) -> None:
|
||||||
"""Mark token as used."""
|
"""Mark token as used."""
|
||||||
await execute(
|
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"])
|
await mark_token_used(token_data["id"])
|
||||||
|
|
||||||
# Update last login
|
# 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
|
# Set session
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Payment provider: paddle
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from paddle_billing import Client as PaddleClient
|
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 quart import Blueprint, flash, g, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
|
||||||
from ..auth.routes import login_required
|
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
|
from ..i18n import get_translations
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ async def upsert_subscription(
|
|||||||
current_period_end: str = None,
|
current_period_end: str = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Create or update subscription. Finds existing by provider_subscription_id."""
|
"""Create or update subscription. Finds existing by provider_subscription_id."""
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
existing = await fetch_one(
|
existing = await fetch_one(
|
||||||
"SELECT id FROM subscriptions WHERE provider_subscription_id = ?",
|
"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:
|
async def update_subscription_status(provider_subscription_id: str, status: str, **extra) -> None:
|
||||||
"""Update subscription status by provider subscription ID."""
|
"""Update subscription status by provider subscription ID."""
|
||||||
extra["updated_at"] = datetime.utcnow().isoformat()
|
extra["updated_at"] = utcnow_iso()
|
||||||
extra["status"] = status
|
extra["status"] = status
|
||||||
sets = ", ".join(f"{k} = ?" for k in extra)
|
sets = ", ".join(f"{k} = ?" for k in extra)
|
||||||
values = list(extra.values())
|
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)
|
base_plan, tier = _derive_tier_from_plan(plan)
|
||||||
monthly_credits = PLAN_MONTHLY_CREDITS.get(base_plan, 0)
|
monthly_credits = PLAN_MONTHLY_CREDITS.get(base_plan, 0)
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
async with db_transaction() as db:
|
async with db_transaction() as db:
|
||||||
# Update supplier record — Basic tier also gets is_verified = 1
|
# 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)."""
|
"""Handle one-time transaction completion (credit packs, sticky boosts, business plan)."""
|
||||||
supplier_id = custom_data.get("supplier_id")
|
supplier_id = custom_data.get("supplier_id")
|
||||||
user_id = custom_data.get("user_id")
|
user_id = custom_data.get("user_id")
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
for item in items:
|
for item in items:
|
||||||
@@ -412,10 +412,8 @@ async def _handle_transaction_completed(data: dict, custom_data: dict) -> None:
|
|||||||
|
|
||||||
# Sticky boost purchases
|
# Sticky boost purchases
|
||||||
elif key == "boost_sticky_week" and supplier_id:
|
elif key == "boost_sticky_week" and supplier_id:
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from ..core import transaction as db_transaction
|
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", "")
|
country = custom_data.get("sticky_country", "")
|
||||||
async with db_transaction() as db:
|
async with db_transaction() as db:
|
||||||
await db.execute(
|
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:
|
elif key == "boost_sticky_month" and supplier_id:
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from ..core import transaction as db_transaction
|
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", "")
|
country = custom_data.get("sticky_country", "")
|
||||||
async with db_transaction() as db:
|
async with db_transaction() as db:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import yaml
|
|||||||
from jinja2 import ChainableUndefined, Environment
|
from jinja2 import ChainableUndefined, Environment
|
||||||
|
|
||||||
from ..analytics import fetch_analytics
|
from ..analytics import fetch_analytics
|
||||||
from ..core import execute, fetch_one, slugify
|
from ..core import execute, fetch_one, slugify, utcnow_iso
|
||||||
|
|
||||||
# ── Constants ────────────────────────────────────────────────────────────────
|
# ── Constants ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ async def generate_articles(
|
|||||||
publish_date = start_date
|
publish_date = start_date
|
||||||
published_today = 0
|
published_today = 0
|
||||||
generated = 0
|
generated = 0
|
||||||
now_iso = datetime.now(UTC).isoformat()
|
now_iso = utcnow_iso()
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
for lang in config["languages"]:
|
for lang in config["languages"]:
|
||||||
|
|||||||
@@ -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.
|
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, utcnow_iso
|
||||||
|
|
||||||
from .core import execute, fetch_all, fetch_one, transaction
|
|
||||||
|
|
||||||
# Credit cost per heat tier
|
# Credit cost per heat tier
|
||||||
HEAT_CREDIT_COSTS = {"hot": 35, "warm": 20, "cool": 8}
|
HEAT_CREDIT_COSTS = {"hot": 35, "warm": 20, "cool": 8}
|
||||||
@@ -44,7 +42,7 @@ async def add_credits(
|
|||||||
note: str = None,
|
note: str = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Add credits to a supplier. Returns new balance."""
|
"""Add credits to a supplier. Returns new balance."""
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
async with transaction() as db:
|
async with transaction() as db:
|
||||||
row = await db.execute_fetchall(
|
row = await db.execute_fetchall(
|
||||||
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
|
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
|
||||||
@@ -73,7 +71,7 @@ async def spend_credits(
|
|||||||
note: str = None,
|
note: str = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Spend credits from a supplier. Returns new balance. Raises InsufficientCredits."""
|
"""Spend credits from a supplier. Returns new balance. Raises InsufficientCredits."""
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
async with transaction() as db:
|
async with transaction() as db:
|
||||||
row = await db.execute_fetchall(
|
row = await db.execute_fetchall(
|
||||||
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
|
"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")
|
raise ValueError("Lead not found")
|
||||||
|
|
||||||
cost = lead["credit_cost"] or compute_credit_cost(lead)
|
cost = lead["credit_cost"] or compute_credit_cost(lead)
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
async with transaction() as db:
|
async with transaction() as db:
|
||||||
# Check balance
|
# Check balance
|
||||||
@@ -180,7 +178,7 @@ async def monthly_credit_refill(supplier_id: int) -> int:
|
|||||||
if not row or not row["monthly_credits"]:
|
if not row or not row["monthly_credits"]:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
new_balance = await add_credits(
|
new_balance = await add_credits(
|
||||||
supplier_id,
|
supplier_id,
|
||||||
row["monthly_credits"],
|
row["monthly_credits"],
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Dashboard domain: user dashboard and settings.
|
Dashboard domain: user dashboard and settings.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from quart import Blueprint, flash, g, redirect, render_template, request, url_for
|
from quart import Blueprint, flash, g, redirect, render_template, request, url_for
|
||||||
|
|
||||||
from ..auth.routes import login_required, update_user
|
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
|
from ..i18n import get_translations
|
||||||
|
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
@@ -57,7 +56,7 @@ async def settings():
|
|||||||
await update_user(
|
await update_user(
|
||||||
g.user["id"],
|
g.user["id"],
|
||||||
name=form.get("name", "").strip() or None,
|
name=form.get("name", "").strip() or None,
|
||||||
updated_at=datetime.utcnow().isoformat(),
|
updated_at=utcnow_iso(),
|
||||||
)
|
)
|
||||||
t = get_translations(g.get("lang") or "en")
|
t = get_translations(g.get("lang") or "en")
|
||||||
await flash(t["dash_settings_saved"], "success")
|
await flash(t["dash_settings_saved"], "success")
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
Supplier directory: public, searchable listing of padel court suppliers.
|
Supplier directory: public, searchable listing of padel court suppliers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import UTC, datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from quart import Blueprint, g, make_response, redirect, render_template, request, url_for
|
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
|
from ..i18n import get_translations
|
||||||
|
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
@@ -89,7 +88,7 @@ async def _build_directory_query(q, country, category, region, page, per_page=24
|
|||||||
lang = g.get("lang", "en")
|
lang = g.get("lang", "en")
|
||||||
cat_labels, country_labels, region_labels = get_directory_labels(lang)
|
cat_labels, country_labels, region_labels = get_directory_labels(lang)
|
||||||
|
|
||||||
now = datetime.now(UTC).isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
params: list = []
|
params: list = []
|
||||||
wheres: list[str] = []
|
wheres: list[str] = []
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from ..core import (
|
|||||||
is_disposable_email,
|
is_disposable_email,
|
||||||
is_plausible_phone,
|
is_plausible_phone,
|
||||||
send_email,
|
send_email,
|
||||||
|
utcnow_iso,
|
||||||
)
|
)
|
||||||
from ..i18n import get_translations
|
from ..i18n import get_translations
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ async def suppliers():
|
|||||||
form.get("court_count", 0),
|
form.get("court_count", 0),
|
||||||
form.get("budget", 0),
|
form.get("budget", 0),
|
||||||
form.get("message", ""),
|
form.get("message", ""),
|
||||||
datetime.utcnow().isoformat(),
|
utcnow_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# Notify admin
|
# Notify admin
|
||||||
@@ -147,7 +148,7 @@ async def financing():
|
|||||||
form.get("court_count", 0),
|
form.get("court_count", 0),
|
||||||
form.get("budget", 0),
|
form.get("budget", 0),
|
||||||
form.get("message", ""),
|
form.get("message", ""),
|
||||||
datetime.utcnow().isoformat(),
|
utcnow_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await send_email(
|
await send_email(
|
||||||
@@ -375,7 +376,7 @@ async def quote_request():
|
|||||||
status,
|
status,
|
||||||
credit_cost,
|
credit_cost,
|
||||||
secrets.token_urlsafe(16),
|
secrets.token_urlsafe(16),
|
||||||
datetime.utcnow().isoformat(),
|
utcnow_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -520,7 +521,7 @@ async def verify_quote():
|
|||||||
from ..credits import compute_credit_cost
|
from ..credits import compute_credit_cost
|
||||||
|
|
||||||
credit_cost = compute_credit_cost(dict(lead))
|
credit_cost = compute_credit_cost(dict(lead))
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE lead_requests SET status = 'new', verified_at = ?, credit_cost = ? WHERE id = ?",
|
"UPDATE lead_requests SET status = 'new', verified_at = ?, credit_cost = ? WHERE id = ?",
|
||||||
(now, credit_cost, lead["id"]),
|
(now, credit_cost, lead["id"]),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Planner domain: padel court financial planner + scenario management.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from quart import Blueprint, Response, g, jsonify, render_template, request
|
from quart import Blueprint, Response, g, jsonify, render_template, request
|
||||||
@@ -18,6 +17,7 @@ from ..core import (
|
|||||||
fetch_all,
|
fetch_all,
|
||||||
fetch_one,
|
fetch_one,
|
||||||
get_paddle_price,
|
get_paddle_price,
|
||||||
|
utcnow_iso,
|
||||||
)
|
)
|
||||||
from ..i18n import get_translations
|
from ..i18n import get_translations
|
||||||
from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
|
from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
|
||||||
@@ -502,7 +502,7 @@ async def save_scenario():
|
|||||||
location = form.get("location", "")
|
location = form.get("location", "")
|
||||||
scenario_id = form.get("scenario_id")
|
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
|
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
|
@login_required
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
async def delete_scenario(scenario_id: int):
|
async def delete_scenario(scenario_id: int):
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE scenarios SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL",
|
"UPDATE scenarios SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL",
|
||||||
(now, scenario_id, g.user["id"]),
|
(now, scenario_id, g.user["id"]),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from datetime import date, timedelta
|
from datetime import UTC, date, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
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:
|
def seed_data_rows(conn: sqlite3.Connection, template_ids: dict[str, int]) -> int:
|
||||||
"""Insert template_data rows for all cities × languages. Returns count inserted."""
|
"""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
|
inserted = 0
|
||||||
|
|
||||||
en_id = template_ids.get("city-padel-cost-en")
|
en_id = template_ids.get("city-padel-cost-en")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Usage:
|
|||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -292,7 +292,7 @@ def main():
|
|||||||
conn.execute("PRAGMA foreign_keys=ON")
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
# 1. Create dev user
|
# 1. Create dev user
|
||||||
print("Creating dev user (dev@localhost)...")
|
print("Creating dev user (dev@localhost)...")
|
||||||
@@ -303,7 +303,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
|
"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
|
dev_user_id = cursor.lastrowid
|
||||||
print(f" Created (id={dev_user_id})")
|
print(f" Created (id={dev_user_id})")
|
||||||
@@ -336,7 +336,7 @@ def main():
|
|||||||
s["website"], s["description"], s["category"], s["tier"],
|
s["website"], s["description"], s["category"], s["tier"],
|
||||||
s["credit_balance"], s["monthly_credits"], s["contact_name"],
|
s["credit_balance"], s["monthly_credits"], s["contact_name"],
|
||||||
s["contact_email"], s["years_in_business"], s["project_count"],
|
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
|
supplier_ids[s["slug"]] = cursor.lastrowid
|
||||||
@@ -349,7 +349,7 @@ def main():
|
|||||||
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
|
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
|
||||||
("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"),
|
("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:
|
for slug, plan, email, name in claimed_suppliers:
|
||||||
sid = supplier_ids.get(slug)
|
sid = supplier_ids.get(slug)
|
||||||
if not sid:
|
if not sid:
|
||||||
@@ -364,14 +364,14 @@ def main():
|
|||||||
else:
|
else:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
|
"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
|
owner_id = cursor.lastrowid
|
||||||
|
|
||||||
# Claim the supplier
|
# Claim the supplier
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL",
|
"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
|
# Create billing customer record
|
||||||
@@ -382,7 +382,7 @@ def main():
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO billing_customers (user_id, provider_customer_id, created_at)
|
"""INSERT INTO billing_customers (user_id, provider_customer_id, created_at)
|
||||||
VALUES (?, ?, ?)""",
|
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
|
# Create active subscription
|
||||||
@@ -396,7 +396,7 @@ def main():
|
|||||||
current_period_end, created_at)
|
current_period_end, created_at)
|
||||||
VALUES (?, ?, 'active', ?, ?, ?)""",
|
VALUES (?, ?, 'active', ?, ?, ?)""",
|
||||||
(owner_id, plan, f"sub_dev_{slug}",
|
(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})")
|
print(f" {slug} -> owner {email} ({plan})")
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
Uses an API key for auth. Fetches query stats and page stats.
|
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
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from ..core import config, execute
|
from ..core import config, execute, utcnow, utcnow_iso
|
||||||
|
|
||||||
_TIMEOUT_SECONDS = 30
|
_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:
|
if not config.BING_WEBMASTER_API_KEY or not config.BING_SITE_URL:
|
||||||
return 0 # Bing not configured — skip silently
|
return 0 # Bing not configured — skip silently
|
||||||
|
|
||||||
started_at = datetime.utcnow()
|
started_at = utcnow()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rows_synced = 0
|
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):
|
if not isinstance(entries, list):
|
||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
cutoff = datetime.utcnow() - timedelta(days=days_back)
|
cutoff = utcnow() - timedelta(days=days_back)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
# Bing date format: "/Date(1708905600000)/" (ms since epoch)
|
# Bing date format: "/Date(1708905600000)/" (ms since epoch)
|
||||||
date_str = entry.get("Date", "")
|
date_str = entry.get("Date", "")
|
||||||
if "/Date(" in date_str:
|
if "/Date(" in date_str:
|
||||||
ms = int(date_str.split("(")[1].split(")")[0])
|
ms = int(date_str.split("(")[1].split(")")[0])
|
||||||
entry_date = datetime.utcfromtimestamp(ms / 1000)
|
entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
|
|||||||
date_str = entry.get("Date", "")
|
date_str = entry.get("Date", "")
|
||||||
if "/Date(" in date_str:
|
if "/Date(" in date_str:
|
||||||
ms = int(date_str.split("(")[1].split(")")[0])
|
ms = int(date_str.split("(")[1].split(")")[0])
|
||||||
entry_date = datetime.utcfromtimestamp(ms / 1000)
|
entry_date = datetime.fromtimestamp(ms / 1000, tz=UTC)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -122,21 +122,21 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
|
|||||||
)
|
)
|
||||||
rows_synced += 1
|
rows_synced += 1
|
||||||
|
|
||||||
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
|
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
|
||||||
await execute(
|
await execute(
|
||||||
"""INSERT INTO seo_sync_log
|
"""INSERT INTO seo_sync_log
|
||||||
(source, status, rows_synced, started_at, completed_at, duration_ms)
|
(source, status, rows_synced, started_at, completed_at, duration_ms)
|
||||||
VALUES ('bing', 'success', ?, ?, ?, ?)""",
|
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
|
return rows_synced
|
||||||
|
|
||||||
except Exception as exc:
|
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(
|
await execute(
|
||||||
"""INSERT INTO seo_sync_log
|
"""INSERT INTO seo_sync_log
|
||||||
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
|
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
|
||||||
VALUES ('bing', 'failed', 0, ?, ?, ?, ?)""",
|
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
|
raise
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlparse
|
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
|
# GSC returns max 25K rows per request
|
||||||
_ROWS_PER_PAGE = 25_000
|
_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:
|
if not config.GSC_SERVICE_ACCOUNT_PATH or not config.GSC_SITE_URL:
|
||||||
return 0 # GSC not configured — skip silently
|
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
|
# 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")
|
end_date = (utcnow() - timedelta(days=2)).strftime("%Y-%m-%d")
|
||||||
start_date = (datetime.utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
|
start_date = (utcnow() - timedelta(days=days_back + 2)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rows = await asyncio.to_thread(
|
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
|
rows_synced += 1
|
||||||
|
|
||||||
duration_ms = int((datetime.utcnow() - started_at).total_seconds() * 1000)
|
duration_ms = int((utcnow() - started_at).total_seconds() * 1000)
|
||||||
await execute(
|
await execute(
|
||||||
"""INSERT INTO seo_sync_log
|
"""INSERT INTO seo_sync_log
|
||||||
(source, status, rows_synced, started_at, completed_at, duration_ms)
|
(source, status, rows_synced, started_at, completed_at, duration_ms)
|
||||||
VALUES ('gsc', 'success', ?, ?, ?, ?)""",
|
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
|
return rows_synced
|
||||||
|
|
||||||
except Exception as exc:
|
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(
|
await execute(
|
||||||
"""INSERT INTO seo_sync_log
|
"""INSERT INTO seo_sync_log
|
||||||
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
|
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
|
||||||
VALUES ('gsc', 'failed', 0, ?, ?, ?, ?)""",
|
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
|
raise
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ All heavy lifting happens in SQL. Functions accept filter parameters
|
|||||||
and return plain dicts/lists.
|
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:
|
def _date_cutoff(date_range_days: int) -> str:
|
||||||
"""Return ISO date string for N days ago."""
|
"""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(
|
async def get_search_performance(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from ..core import config, execute
|
from ..core import config, execute, utcnow, utcnow_iso
|
||||||
|
|
||||||
_TIMEOUT_SECONDS = 15
|
_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:
|
if not config.UMAMI_API_TOKEN or not config.UMAMI_API_URL:
|
||||||
return 0 # Umami not configured — skip silently
|
return 0 # Umami not configured — skip silently
|
||||||
|
|
||||||
started_at = datetime.utcnow()
|
started_at = utcnow()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rows_synced = 0
|
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,
|
# (Umami's metrics endpoint returns totals for the period,
|
||||||
# so we query one day at a time for daily granularity)
|
# so we query one day at a time for daily granularity)
|
||||||
for day_offset in range(days_back):
|
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")
|
metric_date = day.strftime("%Y-%m-%d")
|
||||||
start_ms = int(day.replace(hour=0, minute=0, second=0).timestamp() * 1000)
|
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)
|
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),
|
(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(
|
await execute(
|
||||||
"""INSERT INTO seo_sync_log
|
"""INSERT INTO seo_sync_log
|
||||||
(source, status, rows_synced, started_at, completed_at, duration_ms)
|
(source, status, rows_synced, started_at, completed_at, duration_ms)
|
||||||
VALUES ('umami', 'success', ?, ?, ?, ?)""",
|
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
|
return rows_synced
|
||||||
|
|
||||||
except Exception as exc:
|
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(
|
await execute(
|
||||||
"""INSERT INTO seo_sync_log
|
"""INSERT INTO seo_sync_log
|
||||||
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
|
(source, status, rows_synced, error, started_at, completed_at, duration_ms)
|
||||||
VALUES ('umami', 'failed', 0, ?, ?, ?, ?)""",
|
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
|
raise
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ NOT behind @role_required: Resend posts here unauthenticated.
|
|||||||
Verification uses RESEND_WEBHOOK_SECRET via the Resend SDK.
|
Verification uses RESEND_WEBHOOK_SECRET via the Resend SDK.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import resend
|
import resend
|
||||||
from quart import Blueprint, jsonify, request
|
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")
|
bp = Blueprint("webhooks", __name__, url_prefix="/webhooks")
|
||||||
|
|
||||||
@@ -67,7 +65,7 @@ async def _handle_delivery_event(event_type: str, data: dict) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
last_event, ts_col = _EVENT_UPDATES[event_type]
|
last_event, ts_col = _EVENT_UPDATES[event_type]
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
|
|
||||||
if ts_col:
|
if ts_col:
|
||||||
await execute(
|
await execute(
|
||||||
@@ -87,7 +85,7 @@ async def _handle_inbound(data: dict) -> None:
|
|||||||
if not resend_id:
|
if not resend_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"""INSERT OR IGNORE INTO inbound_emails
|
"""INSERT OR IGNORE INTO inbound_emails
|
||||||
(resend_id, message_id, in_reply_to, from_addr, to_addr,
|
(resend_id, message_id, in_reply_to, from_addr, to_addr,
|
||||||
|
|||||||
@@ -7,7 +7,17 @@ import json
|
|||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime, timedelta
|
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
|
from .i18n import get_translations
|
||||||
|
|
||||||
# Task handlers registry
|
# 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.
|
preheader: hidden preview text shown in email client list views.
|
||||||
"""
|
"""
|
||||||
year = datetime.utcnow().year
|
year = utcnow().year
|
||||||
tagline = _t("email_footer_tagline", lang)
|
tagline = _t("email_footer_tagline", lang)
|
||||||
copyright_text = _t("email_footer_copyright", lang, year=year, app_name=config.APP_NAME)
|
copyright_text = _t("email_footer_copyright", lang, year=year, app_name=config.APP_NAME)
|
||||||
# Hidden preheader trick: visible text + invisible padding to prevent
|
# 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,
|
task_name,
|
||||||
json.dumps(payload or {}),
|
json.dumps(payload or {}),
|
||||||
(run_at or datetime.utcnow()).isoformat(),
|
(run_at or utcnow()).strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
datetime.utcnow().isoformat(),
|
utcnow_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_pending_tasks(limit: int = 10) -> list[dict]:
|
async def get_pending_tasks(limit: int = 10) -> list[dict]:
|
||||||
"""Get pending tasks ready to run."""
|
"""Get pending tasks ready to run."""
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
return await fetch_all(
|
return await fetch_all(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM tasks
|
SELECT * FROM tasks
|
||||||
@@ -156,7 +166,7 @@ async def mark_complete(task_id: int) -> None:
|
|||||||
"""Mark task as completed."""
|
"""Mark task as completed."""
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE tasks SET status = 'complete', completed_at = ? WHERE id = ?",
|
"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:
|
if retries < max_retries:
|
||||||
# Exponential backoff: 1min, 5min, 25min
|
# Exponential backoff: 1min, 5min, 25min
|
||||||
delay = timedelta(minutes=5**retries)
|
delay = timedelta(minutes=5**retries)
|
||||||
run_at = datetime.utcnow() + delay
|
run_at = utcnow() + delay
|
||||||
|
|
||||||
await execute(
|
await execute(
|
||||||
"""
|
"""
|
||||||
@@ -385,13 +395,13 @@ async def handle_send_waitlist_confirmation(payload: dict) -> None:
|
|||||||
@task("cleanup_expired_tokens")
|
@task("cleanup_expired_tokens")
|
||||||
async def handle_cleanup_tokens(payload: dict) -> None:
|
async def handle_cleanup_tokens(payload: dict) -> None:
|
||||||
"""Clean up expired auth tokens."""
|
"""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")
|
@task("cleanup_rate_limits")
|
||||||
async def handle_cleanup_rate_limits(payload: dict) -> None:
|
async def handle_cleanup_rate_limits(payload: dict) -> None:
|
||||||
"""Clean up old rate limit entries."""
|
"""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,))
|
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
|
# Update email_sent_at on lead_forward
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE lead_forwards SET email_sent_at = ? WHERE lead_id = ? AND supplier_id = ?",
|
"UPDATE lead_forwards SET email_sent_at = ? WHERE lead_id = ? AND supplier_id = ?",
|
||||||
(now, lead_id, 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)
|
file_path.write_bytes(pdf_bytes)
|
||||||
|
|
||||||
# Update record
|
# Update record
|
||||||
now = datetime.utcnow().isoformat()
|
now = utcnow_iso()
|
||||||
await execute(
|
await execute(
|
||||||
"UPDATE business_plan_exports SET status = 'ready', file_path = ?, completed_at = ? WHERE id = ?",
|
"UPDATE business_plan_exports SET status = 'ready', file_path = ?, completed_at = ? WHERE id = ?",
|
||||||
(str(file_path), now, export_id),
|
(str(file_path), now, export_id),
|
||||||
@@ -664,7 +674,7 @@ async def handle_generate_business_plan(payload: dict) -> None:
|
|||||||
@task("cleanup_old_tasks")
|
@task("cleanup_old_tasks")
|
||||||
async def handle_cleanup_tasks(payload: dict) -> None:
|
async def handle_cleanup_tasks(payload: dict) -> None:
|
||||||
"""Clean up completed/failed tasks older than 7 days."""
|
"""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(
|
await execute(
|
||||||
"DELETE FROM tasks WHERE status IN ('complete', 'failed') AND created_at < ?", (cutoff,)
|
"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")
|
await enqueue("cleanup_old_tasks")
|
||||||
|
|
||||||
# Monthly credit refill — run on the 1st of each month
|
# Monthly credit refill — run on the 1st of each month
|
||||||
from datetime import datetime
|
today = utcnow()
|
||||||
|
|
||||||
today = datetime.utcnow()
|
|
||||||
this_month = f"{today.year}-{today.month:02d}"
|
this_month = f"{today.year}-{today.month:02d}"
|
||||||
if today.day == 1 and last_credit_refill != this_month:
|
if today.day == 1 and last_credit_refill != this_month:
|
||||||
await enqueue("refill_monthly_credits")
|
await enqueue("refill_monthly_credits")
|
||||||
|
|||||||
Reference in New Issue
Block a user