Share one DB connection per run + cleanups

Open one psycopg2 connection in main() and pass it through pull_data_and_log
and log_to_database. Previously each landing-zone insert opened its own
connection (3-4 handshakes per invocation); now everything runs in a single
session and the three LZ writes share one transaction.

Other cleanups:
- Replace bare `except:` blocks around attraction-field reads with .get()
- Drop the unused col_names computation in the old generic_query
- Move module-level driver code under `if __name__ == "__main__":`
- Parse park_opens / park_closes once instead of three times
- Name columns explicitly in the park-hours query (was SELECT *)
- Use `datestamp` (date column) for the today-freshness check rather
  than reading .date() off the open timestamp

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wes
2026-05-07 06:36:31 -04:00
co-authored by Claude Opus 4.7
parent 10bdee8e1f
commit 7a92834c8b
+103 -106
View File
@@ -1,12 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json
import logging
import os import os
from datetime import datetime, date, timedelta import sys
import json, requests, psycopg2, logging, sys from datetime import date, datetime, timedelta
import psycopg2
import requests
from psycopg2 import sql from psycopg2 import sql
ATTRACTIONS_IO_ENDPOINT = "https://live-data.attractions.io/72f0ea9e-d196-508a-bee8-cce62c3228c7.json" ATTRACTIONS_IO_ENDPOINT = "https://live-data.attractions.io/72f0ea9e-d196-508a-bee8-cce62c3228c7.json"
DISCORD_WEBHOOK = os.environ.get('DISCORD_WEBHOOK', '').strip() DISCORD_WEBHOOK = os.environ.get('DISCORD_WEBHOOK', '').strip()
LOG_PATH = os.environ.get('LOG_PATH', '/var/log/kuh-no-bowls.log')
DB_PASSWORD = os.environ.get('DB_PASSWORD') DB_PASSWORD = os.environ.get('DB_PASSWORD')
if not DB_PASSWORD: if not DB_PASSWORD:
@@ -19,64 +25,44 @@ db_params = {
'password': DB_PASSWORD, 'password': DB_PASSWORD,
'host': os.environ.get('DB_HOST', '192.168.88.9'), 'host': os.environ.get('DB_HOST', '192.168.88.9'),
'port': os.environ.get('DB_PORT', '5432'), 'port': os.environ.get('DB_PORT', '5432'),
'options': '-c search_path=knoebels' 'options': '-c search_path=knoebels',
} }
LOG_PATH = os.environ.get('LOG_PATH', '/var/log/kuh-no-bowls.log') PARK_HOURS_QUERY = """
SELECT datestamp, _id, open, close, closed
park_hours_query = """
SELECT *
FROM knoebels."LZ_attractions_io_resort" FROM knoebels."LZ_attractions_io_resort"
ORDER BY datestamp DESC ORDER BY datestamp DESC
LIMIT 1 LIMIT 1
""" """
API_TIME_FMT = "%Y-%m-%d %H:%M:%S"
def send_to_discord(message): def send_to_discord(message):
if not DISCORD_WEBHOOK: if not DISCORD_WEBHOOK:
# Discord notifications disabled — skip silently.
return return
data = {
"content": message
}
try: try:
response = requests.post(DISCORD_WEBHOOK, json=data) response = requests.post(DISCORD_WEBHOOK, json={"content": message})
if response.status_code != 204:
if response.status_code == 204: logging.warning("Discord webhook returned %s: %s", response.status_code, response.text)
print("Message sent successfully.")
else:
print(f"Failed to send message. Status code: {response.status_code}, Response: {response.text}")
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"Error sending message: {e}") logging.warning("Discord webhook error: %s", e)
def log_to_database(table_name, data):
with psycopg2.connect(**db_params) as conn: def log_to_database(conn, table_name, data):
with conn.cursor() as cursor: if not data:
columns = len(data[0]) return
query = sql.SQL("INSERT INTO {table} VALUES ({values})").format( query = sql.SQL("INSERT INTO {table} VALUES ({values})").format(
table=sql.Identifier(table_name), table=sql.Identifier(table_name),
values=sql.SQL(', ').join(sql.Placeholder() * columns) values=sql.SQL(', ').join(sql.Placeholder() * len(data[0])),
) )
with conn.cursor() as cur:
cursor.executemany(query, data) cur.executemany(query, data)
conn.commit() conn.commit()
def generic_query(q):
ret = []
with psycopg2.connect(**db_params) as conn:
with conn.cursor() as cursor:
cursor.execute(q)
rows = cursor.fetchall()
col_names = [desc[0] for desc in cursor.description]
for row in rows: def fetch_attractions_payload():
ret.append(row) headers = {
return ret
def fetch_webpage(url):
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5', 'Accept-Language': 'en-US,en;q=0.5',
@@ -89,100 +75,111 @@ def fetch_webpage(url):
'Sec-Fetch-Site': 'none', 'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1', 'Sec-Fetch-User': '?1',
} }
try: response = requests.get(ATTRACTIONS_IO_ENDPOINT, headers=headers)
response = requests.get(url, headers=HEADERS) response.raise_for_status()
if response.status_code == 200:
return response.json() return response.json()
else:
return f"Error: {response.status_code} - Unable to fetch webpage"
except Exception as e:
return f"Error: {str(e)} - Unable to fetch webpage"
def pull_data_and_log(log_park_hours):
json_data = fetch_webpage(ATTRACTIONS_IO_ENDPOINT) def _attraction_opening_times(raw):
if not raw:
return None, None
try:
parsed = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return None, None
return parsed.get('start'), parsed.get('end')
def pull_data_and_log(conn, log_park_hours):
json_data = fetch_attractions_payload()
logging.debug('Parsing park hours...') logging.debug('Parsing park hours...')
try: try:
park_opens = json.loads(json_data['entities']['Resort']['records'][0]['OpeningTimes'])['start'] opening_raw = json_data['entities']['Resort']['records'][0]['OpeningTimes']
park_closes = json.loads(json_data['entities']['Resort']['records'][0]['OpeningTimes'])['end'] resort_id = json_data['entities']['Resort']['records'][0]['_id']
except KeyError as e: opening_times = json.loads(opening_raw)
park_opens = opening_times['start']
park_closes = opening_times['end']
except (KeyError, IndexError, json.JSONDecodeError):
logging.error('Unable to pull park hours. Is the park open today?') logging.error('Unable to pull park hours. Is the park open today?')
return return
logging.debug('Checking that park hours from API are actually occuring today...') opens_dt = datetime.strptime(park_opens, API_TIME_FMT)
if not datetime.today().date() == datetime.strptime(park_opens, "%Y-%m-%d %H:%M:%S").date(): closes_dt = datetime.strptime(park_closes, API_TIME_FMT)
if datetime.today().date() != opens_dt.date():
logging.error('API not returning park hours for today yet. Exiting...') logging.error('API not returning park hours for today yet. Exiting...')
return return
resort_data = [[datetime.now(), json_data['entities']['Resort']['records'][0]['_id'], park_opens, park_closes]]
if log_park_hours: if log_park_hours:
logging.info('Inserting park hours into LZ_attractions_io_resort...') logging.info('Inserting park hours into LZ_attractions_io_resort...')
log_to_database('LZ_attractions_io_resort', resort_data) log_to_database(conn, 'LZ_attractions_io_resort',
if not (datetime.strptime(park_opens, "%Y-%m-%d %H:%M:%S") - timedelta(minutes=5)) <= datetime.now() <= (datetime.strptime(park_closes, "%Y-%m-%d %H:%M:%S") + timedelta(minutes=5)): [[datetime.now(), resort_id, park_opens, park_closes]])
if not (opens_dt - timedelta(minutes=5)) <= datetime.now() <= (closes_dt + timedelta(minutes=5)):
logging.info('Park not open now. Exiting...') logging.info('Park not open now. Exiting...')
return return
attraction_data = [] attraction_data = []
for attraction in json_data['entities']['Item']['records']: for attraction in json_data['entities']['Item']['records']:
try: start, end = _attraction_opening_times(attraction.get('OpeningTimes'))
isOperational = attraction['IsOperational'] attraction_data.append([
except: attraction['_id'],
isOperational = None attraction.get('IsOperational'),
try: attraction.get('QueueTime'),
QueueTime = attraction['QueueTime'] attraction.get('QueueStatusMessage'),
except: attraction.get('IsOpen'),
QueueTime = None start,
try: end,
QueueStatusMessage = attraction['QueueStatusMessage'] datetime.now(),
except: ])
QueueStatusMessage = None
try:
IsOpen = attraction['IsOpen']
except:
IsOpen = None
try:
OpeningTimes = json.loads(attraction['OpeningTimes'])
start = OpeningTimes['start']
end = OpeningTimes['end']
except:
start = None
end = None
row = [attraction['_id'], isOperational, QueueTime, QueueStatusMessage, IsOpen, start, end, datetime.now()] queueline_data = [
attraction_data.append(row) [datetime.now(), queue['_id'], queue['QueueTime']]
for queue in json_data['entities']['QueueLine']['records']
queueline_data = [] ]
for queue in json_data['entities']['QueueLine']['records']:
row = [datetime.now(), queue['_id'], queue['QueueTime']]
queueline_data.append(row)
logging.debug('Inserting rows into LZ_attractions_io...') logging.debug('Inserting rows into LZ_attractions_io...')
log_to_database('LZ_attractions_io', attraction_data) log_to_database(conn, 'LZ_attractions_io', attraction_data)
logging.debug('Inserting rows into LZ_attractions_io_queuetimes...') logging.debug('Inserting rows into LZ_attractions_io_queuetimes...')
log_to_database('LZ_attractions_io_queuetimes', queueline_data) log_to_database(conn, 'LZ_attractions_io_queuetimes', queueline_data)
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.DEBUG, filename=LOG_PATH, filemode="a") def main():
logging.basicConfig(
logging.info('--------------------------------------------------------------------------------') format='%(asctime)s %(levelname)s %(message)s',
level=logging.DEBUG,
filename=LOG_PATH,
filemode="a",
)
logging.info('-' * 80)
logging.info('Fetching most recent park hours entry from DB...') logging.info('Fetching most recent park hours entry from DB...')
park_hours_pull = generic_query(park_hours_query)[0] with psycopg2.connect(**db_params) as conn:
logging.debug('%s - %s', park_hours_pull[2], park_hours_pull[3]) with conn.cursor() as cur:
cur.execute(PARK_HOURS_QUERY)
row = cur.fetchone()
if not row:
logging.info('No park hours rows in DB — fetching from API')
pull_data_and_log(conn, log_park_hours=True)
return
datestamp, _id, open_ts, close_ts, _closed = row
logging.debug('%s - %s', open_ts, close_ts)
if datestamp != date.today():
logging.info('Park hours for today not in DB')
pull_data_and_log(conn, log_park_hours=True)
return
if park_hours_pull[2].date() == date.today():
logging.info('Park hours for today already in DB') logging.info('Park hours for today already in DB')
park_opens = park_hours_pull[2] if (open_ts - timedelta(minutes=5)) <= datetime.now() <= (close_ts + timedelta(minutes=5)):
park_closes = park_hours_pull[3]
if (park_opens - timedelta(minutes=5)) <= datetime.now() <= (park_closes + timedelta(minutes=5)):
logging.info('Park is open! Pulling data from API...') logging.info('Park is open! Pulling data from API...')
pull_data_and_log(False) pull_data_and_log(conn, log_park_hours=False)
else: else:
logging.info('Park not currently open. Exiting...') logging.info('Park not currently open. Exiting...')
else:
logging.info('Park hours for today not in DB')
pull_data_and_log(True) if __name__ == '__main__':
main()