Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read from calendars #20

Merged
merged 7 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
History
=======

0.6.1 (unreleased)
0.7.1 (unreleased)
------------------

- Nothing changed yet.


0.7.0 (2024-04-09)
------------------

- Added ``read`` option to ``--execute``


0.6.0 (2023-03-31)
------------------

Expand Down
37 changes: 30 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ B-Open Haunts
What it does
============

Fill Google Calendars with events taken from a Google Spreadsheet.
Fill Google Calendars with events taken from a Google Spreadsheet. Or the other way around.

How to install
==============
Expand Down Expand Up @@ -90,6 +90,18 @@ To just report overtime entries in the set:

haunts --execute report --day=2021-05-24 --day=2021-05-25 --day=2021-05-28 --project="Project X" --overtime May

To *read* today events from all configured calendar and write them on your "May" sheet for the current:

.. code-block:: bash

haunts --execute read May

To *read* events for a specific date from all configured calendar and write them on your "May" sheet for the current:

.. code-block:: bash

haunts --execute read -d 2023-05-15 May

How it works
------------

Expand All @@ -98,8 +110,14 @@ What haunts does depends on the ``--execute`` parameter.
In its default configuration (if ``--execute`` is omitted, or equal to ``sync``), the command will try to access a Google Spreatsheet you must have access to (write access required), specifically: it will read a single sheet at time inside that spreadsheet.
Every row inside this sheet is an event that will be also created on a Google Calendar.

Alternatively you can provide ``--execute report``.
In this case it just access the Google Spreadsheet to collect data.
Alternatively you can provide:

- ``--execute report``.

In this case it just access the Google Spreadsheet to collect data.
- ``--execute read``.

In this case it fills the Google Spreadsheet for you, by *reading* you calendars.

Sheet definition
----------------
Expand Down Expand Up @@ -164,18 +182,23 @@ Every sheet should contains following headers:
Configuring projects
~~~~~~~~~~~~~~~~~~~~

The spreadsheet must also contains a *configuration sheet* (default name is ``config``, can be changed in the .ini) where you must put two columns (with headers):
The spreadsheet must also contains a *configuration sheet* (default name is ``config``, can be changed in the .ini) where you must put at least two columns (with same headers as follows):

**id**
The id of a Google Calendar associated to this project.
You must have write access to this calendar.

**name**
The name of the project, like an alias to the calendar
The name of the project, like a human readable name for a calendar.
A project name can be associated to the same calendar id multiple times (this way you can have aliases).

**read_from** (optional)
User only for ``--execute read``.

A project name can be associated to the same calendar id multiple times.
Read events from this (optional) calendar id instead of the main one.
This makes possible to *read* events from a calendar, but store them in another ones.

Values in the ``name`` column are the only valid values for the ``Project`` column introduced above
Values in the ``name`` column are valid values for the ``Project`` column introduced above.

How events will be filled
-------------------------
Expand Down
21 changes: 18 additions & 3 deletions haunts/calendars.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ def create_event(config_dir, calendar, date, summary, details, length, from_time

from_time = from_time or get("START_TIME", "09:00")
start = datetime.datetime.strptime(
f"{date.strftime('%Y-%m-%d')}T{from_time}:00{LOCAL_TIMEZONE}",
"%Y-%m-%dT%H:%M:%S%z",
f"{date.strftime('%Y-%m-%d')}T{from_time}:00Z",
"%Y-%m-%dT%H:%M:%SZ",
)
print(start)

startParams = None
endParams = None
Expand All @@ -49,19 +50,27 @@ def create_event(config_dir, calendar, date, summary, details, length, from_time
delta = datetime.timedelta(hours=0)
end = start + delta

print(start.isoformat() + "Z")

if haveLength:
# Event with a duration
startParams = {
"dateTime": start.isoformat(),
"timeZone": get("TIMEZONE", "Etc/GMT"),
}
endParams = {
"dateTime": end.isoformat(),
"timeZone": get("TIMEZONE", "Etc/GMT"),
}
else:
# Full day event
startParams = {
"date": start.isoformat()[:10],
"timeZone": get("TIMEZONE", "Etc/GMT"),
}
endParams = {
"date": (end + datetime.timedelta(days=1)).isoformat()[:10],
"timeZone": get("TIMEZONE", "Etc/GMT"),
}

event_body = {
Expand All @@ -73,7 +82,13 @@ def create_event(config_dir, calendar, date, summary, details, length, from_time

def execute_creation():
LOGGER.debug(calendar, date, summary, details, length, event_body, from_time)
event = service.events().insert(calendarId=calendar, body=event_body).execute()
try:
event = (
service.events().insert(calendarId=calendar, body=event_body).execute()
)
except HttpError as err:
LOGGER.error(f"Cannot create the event: {err.status_code}")
raise
return event

try:
Expand Down
9 changes: 8 additions & 1 deletion haunts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .spreadsheet import sync_report
from .report import report
from . import actions
from .download import extract_events


@click.command()
Expand All @@ -35,7 +36,7 @@
@click.option(
"--execute",
"-e",
type=click.Choice(["sync", "report"], case_sensitive=False),
type=click.Choice(["sync", "report", "read"], case_sensitive=False),
help="select which action to execute.",
show_default=True,
default="sync",
Expand Down Expand Up @@ -144,6 +145,12 @@ def main(
)
elif execute == "report":
report(config_dir, sheet, days=day, projects=project, overtime=overtime)
elif execute == "read":
extract_events(
config_dir,
sheet,
day=day[0] if day else datetime.date.today().strftime("%Y-%m-%d"),
)
return 0


Expand Down
115 changes: 115 additions & 0 deletions haunts/download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from googleapiclient.discovery import build
from datetime import datetime, timedelta
import click

from .ini import get
from .credentials import get_credentials
from .spreadsheet import (
append_line,
get_calendars_names,
get_calendars,
)
from .spreadsheet import SCOPES as SPREADSHEET_SCOPES
from .calendars import SCOPES as CALENDAR_SCOPES


def filter_my_event(events):
"""
Take a list of Google Calendar events and returns events created by USER_EMAIL
or events that have USER_EMAIL in the attendees list.
"""
USER_EMAIL = get("USER_EMAIL")
if USER_EMAIL is None:
raise KeyError("USER_EMAIL not set in configuration")
for event in events:
if event.get("creator", {}).get("email") == USER_EMAIL:
yield event
elif USER_EMAIL in [
attendee.get("email") for attendee in event.get("attendees", [])
]:
yield event


def get_events_at(events_service, calendar_id, date):
"""Get all events from a calendar in a specific date."""
start_datetime = datetime.combine(date, datetime.min.time()).isoformat() + "Z"
end_datetime = (
datetime.combine(date, datetime.min.time())
+ timedelta(days=1)
- timedelta(seconds=1)
).isoformat() + "Z"
events_result = events_service.list(
calendarId=calendar_id,
timeMin=start_datetime,
timeMax=end_datetime,
singleEvents=True,
orderBy="startTime",
timeZone=get("TIMEZONE", "Etc/GMT"),
).execute()
events = events_result.get("items", [])
# Enrich events with calendar_id
return [{**e, "calendar_id": calendar_id} for e in events]


def extract_events(config_dir, sheet, day):
"""Public module entry point.

Extract events from Google Calendar and copy them to proper Google Sheet.
"""
calendar_credentials = get_credentials(
config_dir, CALENDAR_SCOPES, "calendars-token.json"
)
spreadsheeet_credentials = get_credentials(
config_dir, SPREADSHEET_SCOPES, "sheets-token.json"
)
calendar_service = build("calendar", "v3", credentials=calendar_credentials)
spreadsheet_service = build("sheets", "v4", credentials=spreadsheeet_credentials)

date_to_check = datetime.strptime(
day, "%Y-%m-%d"
).date() # Replace with the desired date

events_service = calendar_service.events()
sheet_service = spreadsheet_service.spreadsheets()

configured_calendars = get_calendars(
sheet_service, ignore_alias=True, use_read_col=True
)
all_events = []
# Get "my events" from all configured calendars in the selected date
already_added_events = set()
for calendar_id in configured_calendars.values():
events = get_events_at(events_service, calendar_id, date_to_check)
new_events = [
e for e in filter_my_event(events) if e["id"] not in already_added_events
]
already_added_events.update([e["id"] for e in new_events])
all_events.extend(new_events)

# Get calendar configurations
calendar_names = get_calendars_names(sheet_service)

# Main operation loop
for event in all_events:
event_summary = event.get("summary", "No summary")
start = event["start"].get("dateTime", event["start"].get("date"))
end = event["end"].get("dateTime", event["end"].get("date"))
project = calendar_names[event["calendar_id"]]

start_date = datetime.fromisoformat(start).date()
start_time = datetime.fromisoformat(start).time()
duration = datetime.fromisoformat(end) - datetime.fromisoformat(start)
click.echo(f"Adding new event {event_summary} ({project}) to selected sheet")
append_line(
sheet_service,
sheet,
date_col=start_date,
time_col=start_time,
duration_col=duration,
project_col=project,
activity_col=event_summary,
details_col=event.get("description", ""),
event_id_col=event["id"],
link_col=event.get("htmlLink", ""),
action_col="I",
)
8 changes: 8 additions & 0 deletions haunts/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
# Overtime start date in HH:MM format
# Default is empty: no overtime
# OVERTIME_FROM=20:00

# User email.
# Required for `--execute read`
# USER_EMAIL=<your email here>

# Preferred timezone
# Default is GMT
# TIMEZONE=Europe/Rom
"""

parser = configparser.RawConfigParser(allow_no_value=True)
Expand Down
Loading
Loading