Skip to content

Commit 63bab00

Browse files
committed
JSON feed support
Both from flat file and from live feed. Also fixes publishing. Thanks to socallinuxexpo/scale-drupal#244 Signed-off-by: Phil Dibowitz <phil@ipom.com>
1 parent 426ea6b commit 63bab00

File tree

1 file changed

+110
-67
lines changed

1 file changed

+110
-67
lines changed

guidebook/sync_guidebook.py

Lines changed: 110 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/python3
22

33
#
4-
# Copyright 2018 Southern California Linux Expo
4+
# Copyright 2018-present Southern California Linux Expo
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -18,54 +18,70 @@
1818

1919
#
2020
# Author:: Phil Dibowitz <phil@ipm.com>
21-
# This is quick-n-dirty script to import a CSV export from the SCALE
22-
# website into Guidebook. By default it'll add only what's missing, but
23-
# can optionally update all existing sessions.
21+
#
22+
# Script to sync the website schedule to Guidebook complete with region
23+
# mapping.
24+
#
25+
# By default it'll add only what's missing, but can optionally update all
26+
# existing sessions.
2427
#
2528
# It automatically setups rooms ("Locations") and tracks. It has a hard-coded
2629
# map of colors in the Guidebook class, so if you change tracks you'll need
2730
# to update that.
2831
#
2932

33+
from datetime import datetime
3034
import click
31-
import csv
35+
import json
3236
import logging
37+
import os
38+
import pytz
39+
import re
3340
import requests
3441
import sys
35-
import pytz
36-
from datetime import datetime
3742

38-
DBASE_DEFAULT = "/tmp/presentation_exporter_event_1967.csv"
43+
try:
44+
import xdg_base_dirs as xdg
45+
except ImportError:
46+
import xdg
47+
48+
DBASE_DEFAULT = "https://www.socallinuxexpo.org/scale/23x/app"
3949
GUIDE_NAME = "SCaLE 23x"
4050

4151

42-
class OurCSV:
52+
class OurJSON:
4353
rooms = set()
4454
tracks = set()
4555
sessions = set()
4656

4757
FIELD_MAPPING = {
48-
"tracks": "Session Track",
49-
"rooms": "Room/Location",
58+
"tracks": "Track",
59+
"rooms": "Location",
5060
}
5161

52-
def __init__(self, dbase, logger):
62+
def __init__(self, path, logger):
5363
self.logger = logger
54-
self.sessions = self.load_csv(dbase)
64+
if path.startswith("http://") or path.startswith("https://"):
65+
response = requests.get(path)
66+
blob = response.text
67+
self.sessions = self.load_json(blob)
68+
else:
69+
blob = open(path, "r").read()
70+
self.session = self.load_json(blob)
5571

56-
def load_csv(self, filename):
57-
self.logger.info("Loading CSV file")
72+
def load_json(self, raw):
73+
self.logger.info("Loading JSON file")
74+
raw = json.loads(raw)
5875
data = []
59-
with open(filename, "r", encoding="utf-8") as csvfile:
60-
reader = csv.DictReader(csvfile, delimiter=",", quotechar='"')
61-
for row in reader:
62-
track = row[self.FIELD_MAPPING["tracks"]]
63-
room = row[self.FIELD_MAPPING["rooms"]]
64-
if track != "":
65-
self.tracks.add(track)
66-
if room != "":
67-
self.rooms.add(room)
68-
data.append(row)
76+
for session in raw:
77+
track = session[self.FIELD_MAPPING["tracks"]].strip()
78+
room = session[self.FIELD_MAPPING["rooms"]].strip()
79+
if track != "":
80+
self.tracks.add(track)
81+
if room != "":
82+
self.rooms.add(room)
83+
clean_session = {k: v.strip() for k, v in session.items()}
84+
data.append(clean_session)
6985
return data
7086

7187

@@ -366,28 +382,29 @@ def setup_x_map_regions(self):
366382

367383
self.add_x_map_region(map_region, update, rid, location_id)
368384

369-
def to_utc(self, ts):
370-
loc_dt = datetime.strptime(ts, "%Y-%m-%d %H:%M")
371-
pt_dt = pytz.timezone("America/Los_Angeles").localize(loc_dt)
385+
def to_utc(self, ts, fmt):
386+
loc_dt = datetime.strptime(ts, fmt)
387+
if not fmt.endswith("%z"):
388+
pt_dt = pytz.timezone("America/Los_Angeles").localize(loc_dt)
389+
else:
390+
pt_dt = loc_dt
372391
return pt_dt.astimezone(pytz.utc)
373392

374393
def get_times(self, session):
375394
"""
376395
Helper function to build times for guidebook.
377396
"""
378-
d = session["Date"].split()[1]
379-
month, date, year = d.split("/")
380-
381-
start_ts = "%s-%s-%s %s" % (year, month, date, session["Time Start"])
382397

383-
end_ts = "%s-%s-%s %s" % (year, month, date, session["Time End"])
384-
return (self.to_utc(start_ts), self.to_utc(end_ts))
398+
fmt = "%Y-%m-%dT%H:%M:%S%z"
399+
start_ts = session["StartTime"]
400+
end_ts = session["Time End"]
401+
return (self.to_utc(start_ts, fmt), self.to_utc(end_ts, fmt))
385402

386403
def get_id(self, thing, session):
387404
"""
388405
Get the ID for <thing> where thing is a room or track
389406
"""
390-
key = OurCSV.FIELD_MAPPING[thing]
407+
key = OurJSON.FIELD_MAPPING[thing]
391408
if session[key] == "":
392409
return []
393410
self.logger.debug(
@@ -410,14 +427,14 @@ def add_session(self, session, update, sid=None):
410427
"""
411428
if update and not self.update:
412429
return
413-
name = session["Session Title"]
430+
name = session["Name"]
414431
start, end = self.get_times(session)
415432
data = {
416433
"name": name,
417434
"start_time": start,
418435
"end_time": end,
419436
"guide": self.guide,
420-
"description_html": "<p>%s</p>" % session["Description"],
437+
"description_html": "<p>%s</p>" % session["LongAbstract"],
421438
"schedule_tracks": self.get_id("tracks", session),
422439
"locations": self.get_id("rooms", session),
423440
"add_to_schedule": True,
@@ -432,10 +449,10 @@ def setup_sessions(self, sessions):
432449
Add all rooms passed in if missing.
433450
"""
434451
for session in sessions:
435-
name = session["Session Title"]
452+
name = session["Name"]
436453
update = False
437454
sid = None
438-
if session["Date"] == "":
455+
if session["StartTime"] == "":
439456
self.logger.warning("Skipping %s - no date" % name)
440457
continue
441458
if name in self.sessions:
@@ -495,27 +512,51 @@ def delete_all(self):
495512
self.delete_rooms()
496513

497514
def publish_updates(self):
515+
self.logger.info("Publishing changes")
498516
response = requests.post(
499517
self.URLS["publish"].format(guide=self.guide),
500518
headers=self.x_headers,
501519
)
502520

503-
if response.status_code == 204:
521+
if response.status_code == 202:
522+
self.logger.debug("Publish accepted")
504523
return
505524

506-
if (
507-
response.status_code == 403
508-
and "no new content" in resp.text.lower()
509-
):
510-
self.logger.info("No changes to publish")
511-
return
525+
if response.status_code == 403:
526+
resp_text = response.text.lower()
527+
if "no new content" in resp_text:
528+
self.logger.debug("No changes to publish")
529+
return
530+
elif "currently publishing" in resp_text:
531+
self.logger.debug("Guidebook is already publishing")
532+
return
512533

513534
self.logger.error("Failed to publish")
514535
self.logger.error("Status: %s" % response.status_code)
515536
self.logger.error("Body: %s" % response.text)
516537
sys.exit(1)
517538

518539

540+
def _get_token(fname, ename, logger):
541+
env_token = os.getenv(ename)
542+
if env_token is not None:
543+
return env_token.strip()
544+
for dir in xdg.xdg_config_dirs():
545+
api_file = os.path.join(dir, fname)
546+
if os.path.isfile(api_file):
547+
logger.debug("Using %s from %s" % (ename, api_file))
548+
return open(api_file, "r").read().strip()
549+
550+
551+
def get_tokens(logger):
552+
key = _get_token("guidebook_api_token", "GUIDEBOOK_API_TOKEN", logger)
553+
if not key:
554+
logger.critical("No API file specified. See help for details.")
555+
sys.exit(1)
556+
x_key = _get_token("guidebook_jwt_token", "GUIDEBOOK_JWT_TOKEN", logger)
557+
return (key, x_key)
558+
559+
519560
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
520561
@click.option(
521562
"--debug/--no-debug", "-d", default=False, help="Print debug messages."
@@ -531,20 +572,29 @@ def publish_updates(self):
531572
default=False,
532573
help="Delete all tracks, rooms, and sessions",
533574
)
534-
@click.option("--csv-file", default=DBASE_DEFAULT, help="CSV file to use.")
535-
@click.option(
536-
"--api-file",
537-
"-a",
538-
default="guidebook_api.txt",
539-
help="File to read API key from",
540-
)
541575
@click.option(
542-
"--x-api-file",
543-
"-x",
544-
default="guidebook_api_x.txt",
545-
help="File to read API key from",
576+
"--json",
577+
"feed",
578+
metavar="FILE_OR_URL",
579+
default=DBASE_DEFAULT,
580+
help="JSON file or http(s) URL to JSON data.",
546581
)
547-
def main(debug, update, delete_all, csv_file, api_file, x_api_file):
582+
def main(debug, update, delete_all, feed):
583+
"""
584+
Sync the schedule data from our website to Guidebook.
585+
586+
AUTHENTICATION
587+
588+
The Guidebook API token must be provided either via the GUIDEBOOK_API_TOKEN
589+
environment variable, or via a file named 'guidebook_api_token' located in
590+
one of the standard XDG config directories (e.g. ~/.config/).
591+
592+
Optionally, a Guidebook JWT token may be provided via the
593+
GUIDEBOOK_JWT_TOKEN environment variable or a file named
594+
'guidebook_jwt_token' located in one of the standard XDG config
595+
directories. This token is needed for certain operations such as setting up
596+
X Map regions and publishing.
597+
"""
548598
level = logging.INFO
549599
if debug:
550600
level = logging.DEBUG
@@ -555,22 +605,15 @@ def main(debug, update, delete_all, csv_file, api_file, x_api_file):
555605
ch.setFormatter(formatter)
556606
logger.addHandler(ch)
557607

558-
with open(api_file, "r") as api:
559-
key = api.read().strip()
560-
561-
try:
562-
with open(x_api_file, "r") as api:
563-
x_key = api.read().strip()
564-
except IOError:
565-
x_key = None
608+
(key, x_key) = get_tokens(logger)
566609

567610
if delete_all:
568611
print("WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") # noqa: E999
569612
print("This will cause any attendee who has saved any sessions")
570613
print("into a schedule to lose all of that work.")
571614
click.confirm("ARE YOU FUCKING SURE?!", abort=True)
572615
else:
573-
ourdata = OurCSV(csv_file, logger)
616+
ourdata = OurJSON(feed, logger)
574617

575618
ourguide = GuideBook(logger, update, key, x_key=x_key)
576619
if delete_all:

0 commit comments

Comments
 (0)