Created
August 28, 2016 20:01
-
-
Save lak/dd11a346921ae11d6da9bdee656adcbe to your computer and use it in GitHub Desktop.
Simplistic calendar analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
require 'google/apis/calendar_v3' | |
require 'googleauth' | |
require 'googleauth/stores/file_token_store' | |
require 'fileutils' | |
SETTINGS = OpenStruct.new( | |
:oob_uri => 'urn:ietf:wg:oauth:2.0:oob', | |
:application_name => 'LAK Cal Stat', | |
:client_secrets_path => 'client_id.json', | |
:credentials_path => File.join(Dir.home, 'etc', "calstat.yaml"), | |
:scope => Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY, | |
:data_dir => File.join(Dir.home, 'var', 'calstat') | |
) | |
week = 604800 | |
PERIOD = OpenStruct.new( | |
:week => week, | |
:month => week * 4, | |
:quarter => week * 12 | |
) | |
COLORS = [] | |
COLORS[0] = :default | |
COLORS[1] = :transit | |
COLORS[2] = :external | |
COLORS[3] = :company | |
COLORS[4] = :team | |
COLORS[7] = :open | |
COLORS[8] = :notes | |
COLORS[9] = :maybe_interview | |
COLORS[11] = :notice | |
class Reports | |
OUTPUT_TEMPLATE = "%-25s: %s" | |
def self.print(label, data) | |
string = (OUTPUT_TEMPLATE % [label, data]) | |
puts string | |
end | |
# Currently just returning a length of zero for all day events. This is probably sufficient, | |
# but isn't technically correct. It saves on everyone who calls this method needing | |
# to do error handling for this (common) case, and seems to fit my use cases here. | |
def self.event_length(event) | |
start_time = event.start.date_time or return(0) | |
end_time = event.end.date_time or return(0) | |
length = (end_time.to_time.to_f - start_time.to_time.to_f) / 60 / 60 | |
return length | |
end | |
class OpenTime | |
def initialize | |
@time = 0.0 | |
@count = 0 | |
end | |
def process_event(event) | |
return unless event.color_id == "7" | |
@count += 1 | |
@time += Reports.event_length(event) | |
end | |
def print_result | |
Reports.print("Open Time", "%0.2f hours; %i event(s)" % [@time, @count]) | |
end | |
end | |
class BusyEvenings | |
def initialize | |
@days = {} | |
@count = 0 | |
end | |
def process_event(event) | |
unless time = event.end.date_time | |
# puts "Skipping %s because it has no end time" % [event.summary] | |
return | |
end | |
if time.hour > 18 | |
# puts "After-work event at %s: %s (%s)" % [event.end.date_time, event.summary, time.hour] | |
@days[time.day] = true | |
@count += 1 | |
end | |
end | |
def print_result | |
days_busy = @days.length | |
Reports.print("Days busy after work", "%s; Total events: %s" % [days_busy, @count]) | |
end | |
end | |
class WeekendWork | |
def initialize | |
@days = {} | |
@count = 0 | |
@hours = 0.0 | |
end | |
def process_event(event) | |
if event.start.date_time | |
date = event.start.date_time | |
else | |
date = Date.parse(event.start.date) | |
end | |
return unless (date.sunday? or date.saturday?) | |
@days[date.day] = true | |
@count += 1 | |
@hours += Reports.event_length(event) | |
end | |
def print_result | |
days_worked = @days.length | |
Reports.print("Weekend work", "%s days; Total events: %s; total hours: %0.2f" % [days_worked, @count, @hours]) | |
end | |
end | |
end | |
class Calendar | |
attr_reader :credentials, :service, :id, :current_reports | |
## | |
# Ensure valid credentials, either by restoring from the saved credentials | |
# files or intitiating an OAuth2 authorization. If authorization is required, | |
# the user's default browser will be launched to approve the request. | |
# | |
# @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials | |
def authorize | |
FileUtils.mkdir_p(File.dirname(SETTINGS.credentials_path)) | |
client_id = Google::Auth::ClientId.from_file(SETTINGS.client_secrets_path) | |
token_store = Google::Auth::Stores::FileTokenStore.new(file: SETTINGS.credentials_path) | |
authorizer = Google::Auth::UserAuthorizer.new( client_id, SETTINGS.scope, token_store) | |
user_id = 'default' | |
credentials = authorizer.get_credentials(user_id) | |
if credentials.nil? | |
url = authorizer.get_authorization_url( | |
base_url: SETTINGS.oob_uri) | |
puts "Open the following URL in the browser and enter the " + | |
"resulting code after authorization" | |
raise("try 'open' here") | |
puts url | |
code = gets | |
credentials = authorizer.get_and_store_credentials_from_code( | |
user_id: user_id, code: code, base_url: SETTINGS.oob_uri) | |
end | |
@credentials = credentials | |
end | |
def calendar_by_name(name) | |
service.list_calendar_lists().items.each do |cal| | |
return cal if cal.summary == name | |
end | |
raise("Could not find calendar #{name}") | |
end | |
def calendar_id(id) | |
if id == "primary" | |
return id | |
else | |
return calendar_by_name(id).id | |
end | |
end | |
def download_data(period) | |
FileUtils.mkdir_p(File.dirname(SETTINGS.data_dir)) | |
end | |
# Find all of the events for a given week, and send them to the caller. | |
# Assume weeks start on a Sunday and end on a Monday | |
def events_by_week(total) | |
week_dates(total).each do |week, start_time, end_time| | |
init_week(week, start_time) | |
response = service.list_events(id, | |
single_events: true, | |
order_by: 'startTime', | |
time_min: start_time.to_time.iso8601, | |
time_max: end_time.to_time.iso8601) | |
puts "No events found" if response.items.empty? | |
response.items.each do |event| | |
yield event | |
end | |
end | |
end | |
def initialize(name) | |
@id = calendar_id(name) | |
init_service() | |
@reports = [] | |
@weekly_data = [] | |
end | |
def init_service | |
# Initialize the API | |
service = Google::Apis::CalendarV3::CalendarService.new | |
service.client_options.application_name = SETTINGS.application_name | |
service.authorization = authorize() or raise("Failed to get credentials") | |
@service = service | |
end | |
def init_week(week, date) | |
reports = @reports.collect { |k| k.new } | |
@weekly_data.push([ | |
week, | |
date, | |
reports | |
]) | |
# Store the current week's reports so that they get processed by default | |
@current_reports = reports | |
end | |
def list_calendars | |
calendars = service.list_calendar_lists() | |
puts calendars.items.collect { |cal| | |
if cal.description | |
"#{cal.summary}: #{cal.description}" | |
else | |
cal.summary | |
end | |
}.sort | |
exit | |
end | |
def process(count) | |
events_by_week(count) do |event| | |
current_reports.each do |report| | |
report.process_event(event) | |
end | |
end | |
end | |
# Process each of our reports, and print their output | |
def report(count) | |
process(count) | |
@weekly_data.each do |week, date, reports| | |
puts "Week #{week}: #{date.to_date}" | |
reports.each do |report| | |
report.print_result | |
end | |
end | |
end | |
# Add one or more reports to run. Must be the class of a report | |
def report_on(*list) | |
list.each do |klass| | |
@reports.push(klass) | |
end | |
end | |
# This is basically a demo method. Currently unused. | |
def test | |
# Fetch the next 10 events for the user | |
calendar_id = 'primary' | |
response = service.list_events(calendar_id, | |
max_results: 10, | |
single_events: true, | |
order_by: 'startTime', | |
time_min: Time.now.iso8601) | |
puts "Upcoming events:" | |
puts "No upcoming events found" if response.items.empty? | |
response.items.each do |event| | |
start = event.start.date || event.start.date_time | |
puts "- #{event.summary} (#{start})" | |
end | |
end | |
# This method is quite prone to error. That is, it should work in most cases, but I'd be shocked it if it worked in | |
# all of them. At the least, it could use some basic testing for some of the more common failure modes. | |
# It could also probably be done more with declaration and less with math. I assume. | |
def week_dates(distance) | |
# Find the most recent saturday | |
recent_saturday = DateTime.now | |
until recent_saturday.saturday? | |
recent_saturday = recent_saturday.prev_day | |
end | |
dates = [] | |
distance.to_i.times do |i| | |
# Now find $distance saturdays back | |
saturday = recent_saturday - (7 * i) | |
raise("Moved back saturdays to a non-saturday, somehow: #{saturday}") unless saturday.saturday? | |
# Now make a new date, with the above date's day/month/etc, but with the time as 23:59:59 | |
end_time = DateTime.new(saturday.year, saturday.month, saturday.mday, 23, 59, 59) | |
#previous_sunday = (end_time.to_time - (86400 * 6)).to_date | |
previous_sunday = (end_time - 6) | |
raise("Somehow six days ago wasn't a sunday? #{previous_sunday}") unless previous_sunday.sunday? | |
start_time = DateTime.new(previous_sunday.year, previous_sunday.month, previous_sunday.mday, 0, 0, 01) | |
dates.unshift([i, start_time, end_time]) | |
end | |
return dates | |
end | |
end | |
require 'optparse' | |
options = {} | |
OptionParser.new do |opts| | |
opts.banner = "Usage: download.rb [options] <calendar> <number of weeks>" | |
opts.on("-h", "--help", "Print this help string") do |v| | |
puts opts | |
exit | |
end | |
end.parse! | |
calendar_name = ARGV.shift || raise("Must provide calendar") | |
num_weeks = ARGV.shift || raise("Must provide number of weeks") | |
calendar = Calendar.new(calendar_name) | |
calendar.report_on(Reports::OpenTime, Reports::BusyEvenings, Reports::WeekendWork) | |
calendar.report(num_weeks) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment