Skip to content

Commit

Permalink
Refactor code to use new config structure; combine col_to_day and day…
Browse files Browse the repository at this point in the history
…_to_col into get_yesterday_cell; add more/missing type-hints/docstrings
  • Loading branch information
cj-wong committed Apr 29, 2020
1 parent aad03f2 commit 16c377f
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 118 deletions.
87 changes: 45 additions & 42 deletions google/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,54 @@
from typing import Dict, Union

import pendulum
from googleapiclient.discovery import build
from google.oauth2 import service_account
from googleapiclient.discovery import build, Resource

from config import CONF, LOGGER, TODAY, YESTERDAY
import config

# Replaced imports:
# datetime -> pendulum


def get_tab(entry: str, entry_names: Dict[str, list]) -> Union[str, None]:
"""Gets a tab given an `entry` and `entry_names`
to filter, if the entry matches.
def get_tab(entry: str) -> Union[str, None]:
"""Gets a tab given an `entry`, if it exists in `config.TAB_NAMES`.
Args:
entry (str): the name of an entry
entry_names (dict): {tab: [aliases]}
Returns:
str: if a tab was matched
None: if no tabs were matched
str: if `entry` matched a tab in `config.TAB_NAMES`
Raises:
TabNotFound: if `entry` did not match
"""
if entry in entry_names:
return entry
else:
for name, aliases in entry_names.items():
if entry in aliases:
return name
for name, aliases in config.TAB_NAMES.items():
if entry == name or entry in aliases:
return name

raise TabNotFound

return None

class TabNotFound(ValueError):
"""The tab name wasn't found in the configuration. Ignore it."""
pass


class Calendar:
"""Class for managing calendar."""
def __init__(self, credentials) -> None:
"""Class for managing calendar.
Attributes:
interface (Resource): an interface created from credentials;
used to retrieve calendars and entries per calendar
"""
def __init__(self, credentials: service_account.Credentials) -> None:
"""Initialize the Calendar interface.
Args:
credentials: the Google API credentials
credentials (service_account.Credentials): for Google APIs
"""
self.interface = build(
Expand All @@ -61,64 +71,57 @@ def __init__(self, credentials) -> None:
credentials=credentials
)

def get_calendars(self) -> Dict[str, str]:
"""Gets calendars filtered by valid calendar names in config.yaml.
def get_calendar_ids(self) -> Dict[str, str]:
"""Gets IDs for calendars configured in config.yaml. They will
be used for retrieving entries/events per calendar.
Returns:
dict: {summary: id}
Dict[str, str]: {calendar name: calendar id}
"""
cals = {}
cal_names = [
value['calendar']['name']
for value
in CONF['tabs'].values()
]

all_cals = self.interface.calendarList().list().execute()['items']

for cal in all_cals:
if cal['summary'] in cal_names:
cals[cal['summary']] = cal['id']
calendar = cal['summary']
if calendar in config.CALS:
cals[calendar] = cal['id']

return cals

def get_entries(self, cal_id: str, cal_name: str) -> Dict[str, int]:
"""Gets entries in a calendar given `cal_id`
from yesterday until today.
def get_entries(self, cal_name: str, cal_id: str) -> Dict[str, int]:
"""Gets entries in a calendar given `cal_id` from yesterday
until today. We are interested in events that have elapsed
from then and now.
Args:
cal_id (str): the ID of the calendar
cal_name (str): the name (summary) of the calendar
cal_id (str): the ID of the calendar
Returns:
dict: {tab: hours}
"""
tab_hours = defaultdict(int)
entry_names = {
tab: value['calendar']['entry_aliases']
for tab, value
in CONF['tabs'].items()
if value['calendar']['name'] == cal_name
}

all_entries = self.interface.events().list(
calendarId=cal_id,
timeMin=YESTERDAY,
timeMax=TODAY,
timeMin=config.YESTERDAY,
timeMax=config.TODAY,
singleEvents=True,
orderBy='startTime',
).execute()['items']

for entry in all_entries:
tab = get_tab(entry['summary'], entry_names)
if tab is None:
try:
tab = get_tab(entry['summary'])
except TabNotFound:
continue
start = pendulum.parse(entry['start']['dateTime'])
end = pendulum.parse(entry['end']['dateTime'])
tab_hours[tab] += (end - start).seconds/3600
if tab_hours[tab] >= 24:
LOGGER.warning(f'Hours exceeded for tab {tab}')
config.LOGGER.warning(f'Hours exceeded 24 for tab {tab}')

return tab_hours
127 changes: 60 additions & 67 deletions google/sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,53 +12,65 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from typing import Dict
from typing import Dict, Union

import pendulum
from googleapiclient.discovery import build

from config import CONF, LOGGER, YESTERDAY
import config

# Replaced imports:
# datetime -> pendulum

ALPHA = re.compile(r'[a-z]+', re.IGNORECASE)
NUM = re.compile(r'[0-9]+')
ORD_Z = ord('Z')


def col_to_day(col: str) -> int:
"""Converts a column `col` from a str to 1-indexed int (day).
`col` will always be capitalized since `.upper` is called.
def get_yesterday_cell(start: Dict[str, Union[str, int]]) -> str:
"""Retrieve the cell representing yesterday, given the starting
cell and its characteristics. Note that if the cell isn't valid,
`AttributeError` ,`TypeError`, or `ValueError` may be raised,
from attempted string slicing (e.g. `cell[:col_end]`) and type-casts
(e.g. `int(cell[col_end:])`).
Args:
col (str): e.g. 'B' (1), 'C', (2) 'AA' (26)
start (Dict[str, Union[str, int]]): the characteristics of
the starting (top-left) cell in the sheet; contains
'cell' (spreadsheet format), 'year', and 'month'
Returns:
int: the day representation of the column
str: the cell in spreadsheet format, e.g. 'A1'
"""
return ord(col) - 65
cell = start['cell']
first = pendulum.datetime(start['year'], start['month'], 1, tz='local')
col_end = ALPHA.search(cell).end()
col = cell[:col_end].upper()
# We want to perform math on cols to get the right column.
# To do so, we must convert the letters using `ord()`.
ncol = ord(col)
# Columns are 1-indexed, so subtract to get the true offset.
ncol += config.YESTERDAY.day - 1
if ncol <= ORD_Z:
col = chr(ncol)
else: # After Z in columns are AA, AB, etc.
col = f'A{chr(ncol - 26)}'
# `monthy` represents the row given year and month, with offsets
# from `start`.
monthy = int(cell[col_end:])
monthy += (config.YESTERDAY - first).months

return f'{col}{monthy}'


def day_to_col(day: int) -> str:
"""Converts a 1-based day back to str column format.
class Sheets:
"""Class for manging sheets.
Args:
day (int): the day of the month to convert to column
Returns:
str: the column represented by the day
Attributes:
interface (Resource): an interface created from credentials;
used to retrieve spreadsheets and their sheets
"""
day += 65
if day <= 90: # ord('Z')
return chr(day)
else:
return f'A{chr(day-26)}'


class Sheets:
"""Class for manging sheets."""
def __init__(self, credentials) -> None:
"""Initialize the Sheets interface.
Expand All @@ -72,67 +84,48 @@ def __init__(self, credentials) -> None:
credentials=credentials
).spreadsheets()

def get_ids(self, tabs: list) -> Dict[str, str]:
"""Gets sheet IDs filtered by `entry_names`.
Args:
tabs (keys): tab names to filter for
def get_tab_cells(self) -> Dict[str, str]:
"""For all valid tabs, get the cell representing yesterday so
the hours can be recorded there.
Returns:
dict: {name: id}
Dict[str, str]: {tab name: syntax for yesterday's cell}
"""
sheet_ids = {}
spreadsheet = self.interface.get(
spreadsheetId = CONF['spreadsheet_id']
).execute()['sheets']
for sheet in spreadsheet:
properties = sheet['properties']
if properties['title'] in tabs:
sheet_ids[properties['title']] = properties['sheetId']
tab_cells = {}
for tab, conf in config.TABS.items():
start = conf['start']
cell = start['cell']

return sheet_ids
try:
yesterday = get_yesterday_cell(start)
except (AttributeError, TypeError, ValueError) as e:
config.LOGGER.error(f'Skipping {tab}: {e}')
continue

tab_cells[tab] = f'{tab}!{yesterday}'

return tab_cells

def input_hours(self, tab_hours: Dict[str, int]) -> None:
"""Inputs hours given `tab_hours` into their respective sheets.
Args:
tab_hours (dict): {tab: hours}
tab_hours (Dict[str, int]): {tab name: hours}
"""
tab_starts = {}
for tab, value in CONF['tabs'].items():
cell = value['start']['cell']

alpha = ALPHA.search(cell)
col_int = col_to_day(cell[0:alpha.end()].upper())
col_int += YESTERDAY.day - 1
col = day_to_col(col_int)

num = NUM.search(cell)
try:
row = int(cell[alpha.end():num.end()])
except (TypeError, ValueError) as e:
LOGGER.error(f'{e}, Skipping {tab}')
continue
start = pendulum.datetime(
value['start']['year'],
value['start']['month'],
1,
tz='local',
)
row += (YESTERDAY - start).months
tab_starts[tab] = f'{tab}!{col}{row}'

values = self.interface.values()

tab_cells = self.get_tab_cells()

for tab, hour in tab_hours.items():
update = values.update(
spreadsheetId=CONF['spreadsheet_id'],
range=tab_starts[tab],
spreadsheetId=config.SPREADSHEET_ID,
range=tab_cells[tab],
valueInputOption='USER_ENTERED',
body={'values': [[hour]]},
).execute()
LOGGER.info(
config.LOGGER.info(
f"Cells in sheet {tab} updated: {update['updatedCells']}"
)
17 changes: 8 additions & 9 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,19 @@ def main() -> None:
reads calendars, and writes to spreadsheet if records were found.
"""

creds = google.api_handler.authorize()
sheets = google.sheets.Sheets(creds)
calendar = google.calendar.Calendar(creds)
sheet_ids = sheets.get_ids(CONF['tabs'].keys())
cals = calendar.get_calendars()
if not cals:
calendar_api = google.calendar.Calendar(creds)
sheets_api = google.sheets.Sheets(creds)
calendars = calendar_api.get_calendar_ids()
if not calendars:
LOGGER.error(
'No calendars were found matching any in your configuration'
)
for cal_name, cal_id in cals.items():
tab_hours = calendar.get_entries(cal_id, cal_name)
return
for cal_name, cal_id in calendars.items():
tab_hours = calendar_api.get_entries(cal_name, cal_id)
if tab_hours:
sheets.input_hours(tab_hours)
sheets_api.input_hours(tab_hours)
else:
LOGGER.info('No tab-hours were found for yesterday')

Expand Down

0 comments on commit 16c377f

Please sign in to comment.