Skip to content

Commit

Permalink
Add --max-size option to work around Google Calendar limits
Browse files Browse the repository at this point in the history
https://support.google.com/calendar/answer/45654#zippy=%2Cical-file-file-ends-with-ics
says:
> Google Calendar only works with  files that are one megabyte (1MB) or smaller.
  • Loading branch information
liskin committed Feb 8, 2024
1 parent 606d1f8 commit 0e20ada
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 5 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@ omit the `[strava]` bit to avoid installing strava-offline twice.
$ strava-ical --help
Usage: strava-ical [OPTIONS]

Generate iCalendar with your Strava activities

Options:
--csv FILENAME Load activities from CSV instead of the strava-offline database (columns: distance,
elapsed_time, id, moving_time, name, start_date, start_latlng, total_elevation_gain, type)
--strava-database PATH Location of the strava-offline database [default:
/home/user/.local/share/strava_offline/strava.sqlite]
-o, --output FILENAME Output file [default: -]
-m, --max-size SIZE Maximum size of the output file in bytes (accepts K and M suffixes as well)
--help Show this message and exit.
<!-- end include -->

Expand Down
26 changes: 24 additions & 2 deletions src/strava_ical/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from itertools import chain
from pathlib import Path
import re
from typing import BinaryIO
from typing import Optional
from typing import TextIO
Expand All @@ -14,6 +15,21 @@
from .input import read_strava_offline


class SizeType(click.ParamType):
_regex = re.compile(r"(\d+)\s*([KM]?)")
_suffixes = {'': 1, 'K': 1_000, 'M': 1_000_000}

name = "size"

def convert(self, value, param, ctx):
if isinstance(value, int):
return value
elif m := self._regex.fullmatch(value):
return int(m[1]) * self._suffixes[m[2]]
else:
self.fail(f"{value!r} is not a valid size", param, ctx)


@click.command(context_settings={'max_content_width': 120})
@click.option(
'--csv', type=click.File('r'),
Expand All @@ -29,9 +45,15 @@
@click.option(
'-o', '--output', type=click.File('wb'), default='-', show_default=True,
help="Output file")
def cli(csv: Optional[TextIO], strava_database: Path, output: BinaryIO):
@click.option(
'-m', '--max-size', type=SizeType(),
help="Maximum size of the output file in bytes (accepts K and M suffixes as well)")
def cli(csv: Optional[TextIO], strava_database: Path, output: BinaryIO, max_size: Optional[int]):
"""
Generate iCalendar with your Strava activities
"""
if csv:
activities = read_input_csv(csv)
else:
activities = read_strava_offline(strava_database)
output.write(ical(activities))
output.write(ical(activities, max_size=max_size))
11 changes: 10 additions & 1 deletion src/strava_ical/ical.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from typing import Iterable
from typing import Optional

import icalendar # type: ignore [import]

from .data import Activity


def ical(activities: Iterable[Activity]) -> bytes:
def ical(activities: Iterable[Activity], max_size: Optional[int] = None) -> bytes:
cal = icalendar.Calendar()
cal.add('prodid', "strava-ical")
cal.add('version', "2.0")

cal_size = len(cal.to_ical())

for activity in activities:
description = []
if activity.distance > 0:
Expand All @@ -29,6 +32,12 @@ def ical(activities: Iterable[Activity]) -> bytes:
ev.add('description', "\n".join(description))
if activity.start_latlng:
ev.add('geo', activity.start_latlng)

ev_size = len(ev.to_ical())
if max_size is not None and cal_size + ev_size > max_size:
break

cal.add_component(ev)
cal_size += ev_size

return cal.to_ical()
5 changes: 3 additions & 2 deletions src/strava_ical/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def read_input_csv(inp: TextIO) -> Iterable[Activity]:
".mode csv" \
".headers on" \
"SELECT distance, elapsed_time, id, moving_time, name, start_date, \
json_extract(json, '$.start_latlng') AS 'start_latlng', total_elevation_gain, type FROM activity" \
json_extract(json, '$.start_latlng') AS 'start_latlng', total_elevation_gain, type FROM activity \
ORDER BY start_date DESC" \
>activities.csv
"""
for r in csv.DictReader(inp):
Expand All @@ -40,7 +41,7 @@ def read_strava_offline(db_filename: Union[str, PathLike]) -> Iterable[Activity]
with sqlite3.connect(db_filename) as db:
db.row_factory = sqlite3.Row

for r in db.execute('SELECT * FROM activity'):
for r in db.execute('SELECT * FROM activity ORDER BY start_date DESC'):
r_json = json.loads(r['json'])
r = {**r_json, **r}
assert essential_columns <= set(r.keys())
Expand Down
3 changes: 3 additions & 0 deletions tests/readme/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
$ strava-ical --help
Usage: strava-ical [OPTIONS]

Generate iCalendar with your Strava activities

Options:
--csv FILENAME Load activities from CSV instead of the strava-offline database (columns: distance,
elapsed_time, id, moving_time, name, start_date, start_latlng, total_elevation_gain, type)
--strava-database PATH Location of the strava-offline database [default:
/home/user/.local/share/strava_offline/strava.sqlite]
-o, --output FILENAME Output file [default: -]
-m, --max-size SIZE Maximum size of the output file in bytes (accepts K and M suffixes as well)
--help Show this message and exit.
33 changes: 33 additions & 0 deletions tests/test_ical_max_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from strava_ical.data import Activity
from strava_ical.ical import ical


def test_ical():
activities = [
Activity({
'id': 1,
'name': 'Morning Ride',
'distance': 1500,
'total_elevation_gain': 200,
'moving_time': 600,
'elapsed_time': 660,
'start_date': '2024-02-06T10:00:00Z',
'type': 'Ride',
}),
Activity({
'id': 2,
'name': 'Morning Skate',
'distance': 1500,
'total_elevation_gain': 200,
'moving_time': 600,
'elapsed_time': 660,
'start_date': '2024-02-05T10:00:00Z',
'type': 'InlineSkate',
'start_latlng': [51.0, 0.0],
}),
]
empty_size = len(ical([]))
full_size = len(ical(activities))
assert empty_size < full_size
assert len(ical(activities, max_size=full_size)) == full_size
assert empty_size < len(ical(activities, max_size=full_size - 1)) < full_size

0 comments on commit 0e20ada

Please sign in to comment.