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.
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
3034import click
31- import csv
35+ import json
3236import logging
37+ import os
38+ import pytz
39+ import re
3340import requests
3441import 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"
3949GUIDE_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 ["EndTime" ]
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" : 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