fix: use SQLite-compatible space format in utcnow_iso(), fix credits ordering
utcnow_iso() now produces 'YYYY-MM-DD HH:MM:SS' (space separator) matching
SQLite's datetime('now') so lexicographic comparisons like
'published_at <= datetime(now)' work correctly.
Also add `id DESC` tiebreaker to get_ledger() ORDER BY to preserve
insertion order when multiple credits are added within the same second.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
Admin domain: role-based admin panel for managing users, tasks, etc.
|
Admin domain: role-based admin panel for managing users, tasks, etc.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import mistune
|
import mistune
|
||||||
@@ -68,7 +68,7 @@ async def get_dashboard_stats() -> dict:
|
|||||||
"""Get admin dashboard statistics."""
|
"""Get admin dashboard statistics."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
today = now.date().isoformat()
|
today = now.date().isoformat()
|
||||||
week_ago = (now - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
|
week_ago = (now - timedelta(days=7)).strftime("%Y-%m-%d %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",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Auth domain: magic link authentication, user management, decorators.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ async def create_auth_token(user_id: int, token: str, minutes: int = None) -> in
|
|||||||
expires = 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.strftime("%Y-%m-%dT%H:%M:%S")),
|
(user_id, token, expires.strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Payment provider: paddle
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from paddle_billing import Client as PaddleClient
|
from paddle_billing import Client as PaddleClient
|
||||||
@@ -413,7 +413,7 @@ 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 ..core import transaction as db_transaction
|
from ..core import transaction as db_transaction
|
||||||
expires = (utcnow() + timedelta(weeks=1)).strftime("%Y-%m-%dT%H:%M:%S")
|
expires = (utcnow() + timedelta(weeks=1)).strftime("%Y-%m-%d %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(
|
||||||
@@ -429,7 +429,7 @@ 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 ..core import transaction as db_transaction
|
from ..core import transaction as db_transaction
|
||||||
expires = (utcnow() + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S")
|
expires = (utcnow() + timedelta(days=30)).strftime("%Y-%m-%d %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(
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def _validate_table_name(data_table: str) -> None:
|
|||||||
|
|
||||||
def _datetimeformat(value: str, fmt: str = "%Y-%m-%d") -> str:
|
def _datetimeformat(value: str, fmt: str = "%Y-%m-%d") -> str:
|
||||||
"""Jinja2 filter: format a date string (or 'now') with strftime."""
|
"""Jinja2 filter: format a date string (or 'now') with strftime."""
|
||||||
from datetime import UTC, datetime
|
from datetime import datetime
|
||||||
|
|
||||||
if value == "now":
|
if value == "now":
|
||||||
dt = datetime.now(UTC)
|
dt = datetime.now(UTC)
|
||||||
|
|||||||
@@ -102,10 +102,10 @@ def utcnow() -> datetime:
|
|||||||
def utcnow_iso() -> str:
|
def utcnow_iso() -> str:
|
||||||
"""UTC now as naive ISO string for SQLite TEXT columns.
|
"""UTC now as naive ISO string for SQLite TEXT columns.
|
||||||
|
|
||||||
Produces YYYY-MM-DDTHH:MM:SS (no +00:00 suffix) to match the existing
|
Produces YYYY-MM-DD HH:MM:SS (space separator, no +00:00 suffix) to match
|
||||||
format stored in the DB so lexicographic SQL comparisons keep working.
|
SQLite's native datetime('now') format so lexicographic SQL comparisons work.
|
||||||
"""
|
"""
|
||||||
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
|
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -554,12 +554,12 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t
|
|||||||
# Clean old entries and count recent
|
# Clean old entries and count recent
|
||||||
await execute(
|
await execute(
|
||||||
"DELETE FROM rate_limits WHERE key = ? AND timestamp < ?",
|
"DELETE FROM rate_limits WHERE key = ? AND timestamp < ?",
|
||||||
(key, window_start.strftime("%Y-%m-%dT%H:%M:%S")),
|
(key, window_start.strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await fetch_one(
|
result = await fetch_one(
|
||||||
"SELECT COUNT(*) as count FROM rate_limits WHERE key = ? AND timestamp > ?",
|
"SELECT COUNT(*) as count FROM rate_limits WHERE key = ? AND timestamp > ?",
|
||||||
(key, window_start.strftime("%Y-%m-%dT%H:%M:%S")),
|
(key, window_start.strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
)
|
)
|
||||||
count = result["count"] if result else 0
|
count = result["count"] if result else 0
|
||||||
|
|
||||||
@@ -575,7 +575,7 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t
|
|||||||
# Record this request
|
# Record this request
|
||||||
await execute(
|
await execute(
|
||||||
"INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)",
|
"INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)",
|
||||||
(key, now.strftime("%Y-%m-%dT%H:%M:%S")),
|
(key, now.strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
)
|
)
|
||||||
|
|
||||||
return True, info
|
return True, info
|
||||||
@@ -671,7 +671,7 @@ async def hard_delete(table: str, id: int) -> bool:
|
|||||||
|
|
||||||
async def purge_deleted(table: str, days: int = 30) -> int:
|
async def purge_deleted(table: str, days: int = 30) -> int:
|
||||||
"""Purge records deleted more than X days ago."""
|
"""Purge records deleted more than X days ago."""
|
||||||
cutoff = (utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S")
|
cutoff = (utcnow() - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
return await execute(
|
return await execute(
|
||||||
f"DELETE FROM {table} WHERE deleted_at IS NOT NULL AND deleted_at < ?", (cutoff,)
|
f"DELETE FROM {table} WHERE deleted_at IS NOT NULL AND deleted_at < ?", (cutoff,)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -199,6 +199,6 @@ async def get_ledger(supplier_id: int, limit: int = 50) -> list[dict]:
|
|||||||
FROM credit_ledger cl
|
FROM credit_ledger cl
|
||||||
LEFT JOIN lead_forwards lf ON cl.reference_id = lf.id AND cl.event_type = 'lead_unlock'
|
LEFT JOIN lead_forwards lf ON cl.reference_id = lf.id AND cl.event_type = 'lead_unlock'
|
||||||
WHERE cl.supplier_id = ?
|
WHERE cl.supplier_id = ?
|
||||||
ORDER BY cl.created_at DESC LIMIT ?""",
|
ORDER BY cl.created_at DESC, cl.id DESC LIMIT ?""",
|
||||||
(supplier_id, limit),
|
(supplier_id, limit),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Leads domain: capture interest in court suppliers and financing.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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 = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
|
now = datetime.now(UTC).strftime("%Y-%m-%d %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")
|
||||||
|
|||||||
@@ -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.strftime("%Y-%m-%dT%H:%M:%S")),
|
("dev@localhost", "Dev User", now.strftime("%Y-%m-%d %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.strftime("%Y-%m-%dT%H:%M:%S"),
|
s["service_area"], now.strftime("%Y-%m-%d %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)).strftime("%Y-%m-%dT%H:%M:%S")
|
period_end = (now + timedelta(days=30)).strftime("%Y-%m-%d %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.strftime("%Y-%m-%dT%H:%M:%S")),
|
(email, name, now.strftime("%Y-%m-%d %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.strftime("%Y-%m-%dT%H:%M:%S"), sid),
|
(owner_id, now.strftime("%Y-%m-%d %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.strftime("%Y-%m-%dT%H:%M:%S")),
|
(owner_id, f"ctm_dev_{slug}", now.strftime("%Y-%m-%d %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.strftime("%Y-%m-%dT%H:%M:%S")),
|
period_end, now.strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
)
|
)
|
||||||
print(f" {slug} -> owner {email} ({plan})")
|
print(f" {slug} -> owner {email} ({plan})")
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
|
|||||||
"""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.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
|
(rows_synced, started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
|
||||||
)
|
)
|
||||||
return rows_synced
|
return rows_synced
|
||||||
|
|
||||||
@@ -137,6 +137,6 @@ async def sync_bing(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS)
|
|||||||
"""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.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
|
(str(exc), started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ is synchronous, so sync runs in asyncio.to_thread().
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
|
|||||||
"""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.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
|
(rows_synced, started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
|
||||||
)
|
)
|
||||||
return rows_synced
|
return rows_synced
|
||||||
|
|
||||||
@@ -137,6 +137,6 @@ async def sync_gsc(days_back: int = 3, max_pages: int = 10) -> int:
|
|||||||
"""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.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
|
(str(exc), started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Uses bearer token auth. Self-hosted instance, no rate limits.
|
|||||||
Config already exists: UMAMI_API_URL, UMAMI_API_TOKEN, UMAMI_WEBSITE_ID.
|
Config already exists: UMAMI_API_URL, UMAMI_API_TOKEN, UMAMI_WEBSITE_ID.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
|
|||||||
"""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.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
|
(rows_synced, started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
|
||||||
)
|
)
|
||||||
return rows_synced
|
return rows_synced
|
||||||
|
|
||||||
@@ -111,6 +111,6 @@ async def sync_umami(days_back: int = 3, timeout_seconds: int = _TIMEOUT_SECONDS
|
|||||||
"""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.strftime("%Y-%m-%dT%H:%M:%S"), utcnow_iso(), duration_ms),
|
(str(exc), started_at.strftime("%Y-%m-%d %H:%M:%S"), utcnow_iso(), duration_ms),
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ from ..core import (
|
|||||||
config,
|
config,
|
||||||
csrf_protect,
|
csrf_protect,
|
||||||
execute,
|
execute,
|
||||||
|
feature_gate,
|
||||||
fetch_all,
|
fetch_all,
|
||||||
fetch_one,
|
fetch_one,
|
||||||
feature_gate,
|
|
||||||
get_paddle_price,
|
get_paddle_price,
|
||||||
is_flag_enabled,
|
is_flag_enabled,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ 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 utcnow()).strftime("%Y-%m-%dT%H:%M:%S"),
|
(run_at or utcnow()).strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
utcnow_iso(),
|
utcnow_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -401,7 +401,7 @@ async def handle_cleanup_tokens(payload: dict) -> None:
|
|||||||
@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 = (utcnow() - timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%S")
|
cutoff = (utcnow() - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
await execute("DELETE FROM rate_limits WHERE timestamp < ?", (cutoff,))
|
await execute("DELETE FROM rate_limits WHERE timestamp < ?", (cutoff,))
|
||||||
|
|
||||||
|
|
||||||
@@ -674,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 = (utcnow() - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
|
cutoff = (utcnow() - timedelta(days=7)).strftime("%Y-%m-%d %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,)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ sitemap integration, admin CRUD routes, and path collision prevention.
|
|||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import date, datetime
|
from datetime import date
|
||||||
|
|
||||||
from padelnomics.core import utcnow_iso
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -21,7 +19,7 @@ from padelnomics.content.routes import (
|
|||||||
bake_scenario_cards,
|
bake_scenario_cards,
|
||||||
is_reserved_path,
|
is_reserved_path,
|
||||||
)
|
)
|
||||||
from padelnomics.core import execute, fetch_all, fetch_one, slugify
|
from padelnomics.core import execute, fetch_all, fetch_one, slugify, utcnow_iso
|
||||||
from padelnomics.planner.calculator import calc, validate_state
|
from padelnomics.planner.calculator import calc, validate_state
|
||||||
|
|
||||||
SCHEMA_PATH = Path(__file__).parent.parent / "src" / "padelnomics" / "migrations" / "schema.sql"
|
SCHEMA_PATH = Path(__file__).parent.parent / "src" / "padelnomics" / "migrations" / "schema.sql"
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ Tests for the credit system (credits.py).
|
|||||||
|
|
||||||
Pure SQL operations against real in-memory SQLite — no mocking needed.
|
Pure SQL operations against real in-memory SQLite — no mocking needed.
|
||||||
"""
|
"""
|
||||||
from padelnomics.core import utcnow_iso
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from padelnomics.core import utcnow_iso
|
||||||
from padelnomics.credits import (
|
from padelnomics.credits import (
|
||||||
InsufficientCredits,
|
InsufficientCredits,
|
||||||
add_credits,
|
add_credits,
|
||||||
|
|||||||
@@ -7,16 +7,13 @@ Integration tests exercise full request/response flows via Quart test client.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from padelnomics.core import utcnow_iso
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from padelnomics.core import utcnow_iso
|
||||||
from padelnomics import core
|
|
||||||
from padelnomics.migrations.migrate import migrate
|
from padelnomics.migrations.migrate import migrate
|
||||||
|
|
||||||
|
from padelnomics import core
|
||||||
|
|
||||||
# ── Fixtures & helpers ────────────────────────────────────────────
|
# ── Fixtures & helpers ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -294,8 +291,9 @@ class TestLeadUnlockGate:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_route_imports_is_flag_enabled(self):
|
async def test_route_imports_is_flag_enabled(self):
|
||||||
"""suppliers/routes.py imports is_flag_enabled (gate is wired up)."""
|
"""suppliers/routes.py imports is_flag_enabled (gate is wired up)."""
|
||||||
from padelnomics.suppliers.routes import unlock_lead
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from padelnomics.suppliers.routes import unlock_lead
|
||||||
src = inspect.getsource(unlock_lead)
|
src = inspect.getsource(unlock_lead)
|
||||||
assert "is_flag_enabled" in src
|
assert "is_flag_enabled" in src
|
||||||
assert "lead_unlock" in src
|
assert "lead_unlock" in src
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"""Tests for the SEO metrics module: queries, sync functions, admin routes."""
|
"""Tests for the SEO metrics module: queries, sync functions, admin routes."""
|
||||||
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
from padelnomics.core import utcnow_iso
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from padelnomics.core import utcnow_iso
|
||||||
from padelnomics.seo._queries import (
|
from padelnomics.seo._queries import (
|
||||||
cleanup_old_metrics,
|
cleanup_old_metrics,
|
||||||
get_article_scorecard,
|
get_article_scorecard,
|
||||||
|
|||||||
@@ -8,19 +8,16 @@ supervisor.py lives in src/padelnomics/ (not a uv workspace package), so we
|
|||||||
add src/ to sys.path before importing.
|
add src/ to sys.path before importing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
# Load supervisor.py directly by path — avoids clashing with the web app's
|
||||||
|
# 'padelnomics' namespace (which is the installed web package).
|
||||||
|
import importlib.util as _ilu
|
||||||
import textwrap
|
import textwrap
|
||||||
import tomllib
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# Load supervisor.py directly by path — avoids clashing with the web app's
|
|
||||||
# 'padelnomics' namespace (which is the installed web package).
|
|
||||||
import importlib.util as _ilu
|
|
||||||
|
|
||||||
_SUP_PATH = Path(__file__).parent.parent.parent / "src" / "padelnomics" / "supervisor.py"
|
_SUP_PATH = Path(__file__).parent.parent.parent / "src" / "padelnomics" / "supervisor.py"
|
||||||
_spec = _ilu.spec_from_file_location("padelnomics_supervisor", _SUP_PATH)
|
_spec = _ilu.spec_from_file_location("padelnomics_supervisor", _SUP_PATH)
|
||||||
sup = _ilu.module_from_spec(_spec)
|
sup = _ilu.module_from_spec(_spec)
|
||||||
@@ -32,7 +29,6 @@ from padelnomics_extract.proxy import (
|
|||||||
make_sticky_selector,
|
make_sticky_selector,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── load_workflows ────────────────────────────────────────────────
|
# ── load_workflows ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ Uses the existing client, db, sign_payload from conftest.
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from padelnomics.core import utcnow_iso
|
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from conftest import sign_payload
|
from conftest import sign_payload
|
||||||
|
from padelnomics.core import utcnow_iso
|
||||||
|
|
||||||
WEBHOOK_PATH = "/billing/webhook/paddle"
|
WEBHOOK_PATH = "/billing/webhook/paddle"
|
||||||
SIG_HEADER = "Paddle-Signature"
|
SIG_HEADER = "Paddle-Signature"
|
||||||
|
|||||||
Reference in New Issue
Block a user