* Centralize filesystem paths in a single module Add paths.py, which resolves the repo root once and derives the cache, accounts DB, logs, templates, frontend and bundled-city-data locations from it. Replace the 13 per-module `dirname(__file__)/..` anchors with references to it, so a module's location no longer determines where the app reads its data. Env overrides (accounts DB, VAPID, IndexNow) are unchanged; every resolved path is byte-identical to before. Groundwork for moving modules into packages without re-pointing paths. Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62 * Split the backend into domain packages Group the flat backend modules into packages that mirror their concerns: data/ climate, grading, scoring, grid, places, cities, city_events, store web/ app, views, homepage, content, schemas notifications/ notify, digest, push, mailer, discord, discord_interactions, discord_link accounts/ models, users, api_accounts, db core/ metrics, singleton, audit Intra-project imports are rewritten to the package-qualified form. The entry scripts (indexnow, warm_cities, migrate, gen_cities, gen_flavor) and paths.py stay at the backend/ root, and backend/app.py becomes a shim re-exporting web.app:app so the launch target stays `app:app` — run.sh, the systemd units, and CI need no change. Verified: full suite (318) passes, `uvicorn app:app` boots and serves the home/SEO/static/API surfaces, and every root script imports clean. Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
59 lines
2.4 KiB
Python
59 lines
2.4 KiB
Python
"""Pre-warm the archives for the curated city set (backend/cities.json) so the
|
|
crawlable /climate pages render from cache and a search-engine crawl never bursts
|
|
the archive API quota. Run at/after deploy:
|
|
|
|
python warm_cities.py [--limit N] [--pace SECONDS]
|
|
|
|
Idempotent: a cell whose archive is already cached is skipped. Fetches are paced
|
|
(default 2s) to stay well under the archive API's rate limit. A cell that still
|
|
has no cached archive when its page is first requested self-heals via get_history,
|
|
so this is an optimization, not a hard dependency.
|
|
"""
|
|
import sys
|
|
import time
|
|
|
|
from data import cities
|
|
from data import climate
|
|
from data import grid
|
|
from web import homepage
|
|
|
|
|
|
def main(limit: int | None = None, pace: float = 2.0) -> None:
|
|
todo = cities.all_cities()
|
|
if limit:
|
|
todo = todo[:limit]
|
|
fetched = skipped = failed = 0
|
|
for i, c in enumerate(todo, 1):
|
|
cell = grid.snap(c["lat"], c["lon"])
|
|
cached = climate.load_cached_history(cell)
|
|
if cached is not None and not cached.is_empty():
|
|
skipped += 1
|
|
continue
|
|
try:
|
|
climate.get_history(cell) # fetch + cache the ~45-yr archive
|
|
climate.get_recent_forecast(cell) # + the recent/forecast bundle (today block)
|
|
fetched += 1
|
|
print(f"[{i}/{len(todo)}] warmed {c['slug']} ({cell['id']})")
|
|
time.sleep(pace)
|
|
except Exception as e: # noqa: BLE001 - keep going; the page self-heals later
|
|
failed += 1
|
|
print(f"[{i}/{len(todo)}] FAILED {c['slug']}: {e}")
|
|
time.sleep(pace)
|
|
print(f"done: fetched={fetched} skipped(cached)={skipped} failed={failed}")
|
|
|
|
# Rebuild the homepage's "unusual right now" feed from whatever is now warm.
|
|
# Runs here as well as on the notifier loop so a fresh deploy has a populated
|
|
# feed immediately, and so it still refreshes when the notifier is disabled.
|
|
try:
|
|
feed = homepage.refresh()
|
|
print(f"homepage feed: {feed['considered']} cities graded, "
|
|
f"{len(feed['ranked'])} ranked")
|
|
except Exception as e: # noqa: BLE001 - the homepage renders without the feed
|
|
print(f"homepage feed: FAILED {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = sys.argv[1:]
|
|
lim = int(args[args.index("--limit") + 1]) if "--limit" in args else None
|
|
pc = float(args[args.index("--pace") + 1]) if "--pace" in args else 2.0
|
|
main(limit=lim, pace=pc)
|