Skip to content

Commit 4bd7e57

Browse files
authored
Merge pull request internetarchive#10943 from jimchamp/pref-to-store-script
Script for writing preferences to `store` tables
1 parent d87bc72 commit 4bd7e57

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

scripts/migrations/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ Created as a follow-up to [issue #11024](https://github.com/internetarchive/open
99
## `delete_merge_debug.py`
1010

1111
This script was created in support of [issue #10887](https://github.com/internetarchive/openlibrary/issues/10887). When executed, it will delete all entries having `type` `merge-authors-debug` from the `store` and `store_index` tables.
12+
13+
## `write_prefs_to_store.py`
14+
15+
Created as a follow-up to [issue #10920](https://github.com/internetarchive/openlibrary/pull/10920), this script writes
16+
a subset of existing preferences to the store.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Copies all patrons' preferences to the `store` tables.
4+
"""
5+
import argparse
6+
from pathlib import Path
7+
8+
from psycopg2 import DatabaseError
9+
10+
import infogami
11+
from openlibrary.accounts import RunAs
12+
from openlibrary.accounts.model import OpenLibraryAccount
13+
from openlibrary.config import load_config
14+
from openlibrary.core import db
15+
from scripts.utils.graceful_shutdown import init_signal_handler, was_shutdown_requested
16+
17+
DEFAULT_CONFIG_PATH = "/olsystem/etc/openlibrary.yml"
18+
PREFERENCE_TYPE = 'preferences'
19+
20+
21+
def setup(config_path):
22+
init_signal_handler()
23+
if not Path(config_path).exists():
24+
raise FileNotFoundError(f'no config file at {config_path}')
25+
load_config(config_path)
26+
infogami._setup()
27+
28+
29+
def copy_preferences_to_store(keys, verbose: bool = False) -> list[str]:
30+
errors = []
31+
for key in keys:
32+
if was_shutdown_requested():
33+
break
34+
try:
35+
if verbose:
36+
print(f"Writing {key} to store...")
37+
username = key.split('/')[-1]
38+
ol_acct = OpenLibraryAccount.get_by_username(username)
39+
prefs = (ol_acct and ol_acct.get_user().preferences()) or {}
40+
if ol_acct and prefs.get('type', '') != PREFERENCE_TYPE:
41+
prefs['type'] = PREFERENCE_TYPE
42+
prefs['_rev'] = None
43+
44+
with RunAs(username):
45+
ol_acct.get_user().save_preferences(
46+
prefs, msg="Update preferences for store", use_store=True
47+
)
48+
except Exception as e: # noqa: BLE001
49+
print(f"An error occurred while copying preferences to store: {e}")
50+
errors.append(key)
51+
52+
return errors
53+
54+
55+
def _fetch_preference_keys() -> list[str]:
56+
"""
57+
Returns a list of all preference keys that contain a `pda` value but are
58+
not yet persisted in the store.
59+
60+
"""
61+
oldb = db.get_db()
62+
t = oldb.transaction()
63+
64+
tmp_tbl_query = """
65+
CREATE TEMPORARY TABLE temp_preference_ids AS
66+
SELECT thing_id FROM datum_str
67+
WHERE key_id = (
68+
SELECT id FROM property
69+
WHERE name = 'notifications.pda'
70+
)
71+
ORDER BY thing_id ASC
72+
"""
73+
74+
preference_key_join_query = """
75+
SELECT thing.key as key FROM thing
76+
LEFT JOIN store ON thing.key = store.key
77+
WHERE thing.id IN (
78+
SELECT thing_id FROM temp_preference_ids
79+
)
80+
AND store.key IS NULL;
81+
"""
82+
83+
keys = []
84+
try:
85+
# Create temporary table containing `thing` IDs of all affected preference objects
86+
oldb.query(tmp_tbl_query)
87+
88+
missing_store_entries = oldb.query(preference_key_join_query)
89+
keys = [entry.get('key', '') for entry in list(missing_store_entries)]
90+
except DatabaseError as e:
91+
print(f"An error occurred while fetching preference keys: {e}")
92+
t.rollback()
93+
t.rollback()
94+
return keys
95+
96+
97+
def main(args):
98+
print("Setting up connection with DB...")
99+
setup(args.config)
100+
101+
print("Fetching affected preferences...")
102+
affected_pref_keys = _fetch_preference_keys()
103+
104+
print(f"Found {len(affected_pref_keys)} affected preferences")
105+
if args.dry_run:
106+
print("Skipping copy to store step...")
107+
return
108+
109+
print("Copying preferences to store...")
110+
while affected_pref_keys and not was_shutdown_requested():
111+
print(
112+
f"Begin writing batch of {len(affected_pref_keys)} preferences to store..."
113+
)
114+
cur_batch = affected_pref_keys[:1000]
115+
retries = copy_preferences_to_store(cur_batch, verbose=args.verbose)
116+
affected_pref_keys.extend(retries)
117+
print(f"Batch completed with {len(retries)} errors\n")
118+
119+
if was_shutdown_requested():
120+
print("Script terminated early due to shutdown request.")
121+
return
122+
123+
print("All affected preferences have been written to the store.")
124+
125+
126+
def _parse_args():
127+
p = argparse.ArgumentParser(description=__doc__)
128+
p.add_argument(
129+
"-c",
130+
"--config",
131+
default=DEFAULT_CONFIG_PATH,
132+
help="Path to the `openlibrary.yml` configuration file",
133+
)
134+
p.add_argument(
135+
"-d",
136+
"--dry-run",
137+
action="store_true",
138+
help="Enable dry-run mode, which merely prints the number of preference keys that will be written to the store",
139+
)
140+
p.add_argument(
141+
"-v",
142+
"--verbose",
143+
action="store_true",
144+
help="Print each preference key when it is added to the store",
145+
)
146+
p.set_defaults(func=main)
147+
return p.parse_args()
148+
149+
150+
if __name__ == '__main__':
151+
_args = _parse_args()
152+
_args.func(_args)
153+
print("\nScript execution complete. So long and take care!")

0 commit comments

Comments
 (0)