Let a maintenance script find the database the site actually uses

Running `python scripts/export_catalog.py` on a deployed site died with
1045 "Access denied for user 'root'@'localhost'", naming a user that site
does not have.

Nothing loads .env for a plain script run. wsgi.py calls load_dotenv() for the
served app and the flask CLI loads .env for itself, so both reach the real
database; a bare `python scripts/...` gets neither. DATABASE_URL then stays
unset, FLASK_ENV defaults to development, and config.py falls back to a
hardcoded root@localhost URL.

The read-only failure above is the harmless version. Six of these seven
scripts WRITE - backfill, reclassify, retype, collapse, refile, import - and
on any box where those fallback root credentials happen to resolve, they would
have silently rewritten the wrong database with no error at all. That is also
why this survived: the dev .env points at root, so the fallback matched what
the developer expected and nothing ever looked wrong here.

All seven now load .env from the repo root, resolved from __file__ rather than
the working directory, since these are run by hand during a deployment and
rarely from the repo root.

tests/test_maintenance_scripts.py pins it for any future script: every script
that calls create_app must call load_dotenv, before create_app rather than
after it, and with an explicit path rather than a cwd-relative search.
This commit is contained in:
cproudlock
2026-08-24 14:32:11 -04:00
parent 5f915310c9
commit 7d7caba56b
8 changed files with 185 additions and 0 deletions

View File

@@ -33,9 +33,22 @@ Dry run by default; nothing is written without --commit.
import argparse
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
# Each asset table that carries both a model link and its own vendor, with the
# column that identifies a row in reports and logs.
TARGETS = [

View File

@@ -38,11 +38,24 @@ database backup before --commit anyway; this deletes rows.
import argparse
import os
import sys
from pathlib import Path
from collections import defaultdict
# Run from anywhere: put the repo root (parent of scripts/) on the path.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
from shopdb import create_app
from shopdb.api import db
from shopdb.core.models import Asset

View File

@@ -25,10 +25,23 @@ import argparse
import json
import os
import sys
from pathlib import Path
from collections import OrderedDict
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
DEFAULT_OUT = os.path.join('shopdb', 'data', 'catalog.json')

View File

@@ -21,10 +21,23 @@ Dry run by default. Nothing is written without --commit.
import argparse
import os
import sys
from pathlib import Path
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
from shopdb import create_app # noqa: E402
from shopdb.api import db # noqa: E402

View File

@@ -19,10 +19,23 @@ Usage (on the target instance, in the app dir):
import argparse
import os
import sys
from pathlib import Path
# Run from anywhere: put the repo root (parent of scripts/) on the path.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
from shopdb import create_app
from shopdb.api import db
from shopdb.core.models import Asset, AssetType

View File

@@ -35,11 +35,24 @@ Dry run by default. Take a database backup before --commit.
import argparse
import os
import sys
from pathlib import Path
from collections import defaultdict
# Run from anywhere: put the repo root (parent of scripts/) on the path.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
from shopdb import create_app
from shopdb.api import db, Asset, AssetRelationship, RelationshipType
from plugins.backups.models import BackupRevision

View File

@@ -33,9 +33,22 @@ import csv
import os
import re
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load .env before anything reads config. wsgi.py does this for the served app
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
# and config falls back to a hardcoded root@localhost URL. That fails on a real
# deployment with a bare 1045 Access denied for 'root', naming a user the site
# does not use - and on a box where those root credentials happen to work, it
# is worse, because the script silently reads or WRITES the wrong database.
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
# Each asset class, its own type vocabulary, and the modeltypes.category that a
# newly created model type should carry.
CLASSES = {