1
0
mirror of https://github.com/b4tman/sync_ics2gcal synced 2024-09-21 08:58:03 +00:00

Merge pull request #51 from b4tman/feature/type_annotations

Type annotations
This commit is contained in:
Dmitry Belyaev 2021-05-01 15:00:52 +03:00 committed by GitHub
commit bd6bd65719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 153 additions and 130 deletions

4
.gitignore vendored
View File

@ -2,8 +2,10 @@ config.yml
service-account.json service-account.json
*.pyc *.pyc
my-test*.ics my-test*.ics
.vscode/* .vscode/
.idea/
/dist/ /dist/
/*.egg-info/ /*.egg-info/
/build/ /build/
/.eggs/ /.eggs/
venv/

View File

@ -4,9 +4,11 @@ import google.auth
from google.oauth2 import service_account from google.oauth2 import service_account
from googleapiclient import discovery from googleapiclient import discovery
from pytz import utc from pytz import utc
from datetime import datetime
from typing import List, Dict, Any, Callable, Tuple, Optional
class GoogleCalendarService(): class GoogleCalendarService:
"""class for make google calendar service Resource """class for make google calendar service Resource
Returns: Returns:
@ -18,9 +20,6 @@ class GoogleCalendarService():
"""make service Resource from default credentials (authorize) """make service Resource from default credentials (authorize)
( https://developers.google.com/identity/protocols/application-default-credentials ) ( https://developers.google.com/identity/protocols/application-default-credentials )
( https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default ) ( https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default )
Returns:
service Resource
""" """
scopes = ['https://www.googleapis.com/auth/calendar'] scopes = ['https://www.googleapis.com/auth/calendar']
@ -30,11 +29,8 @@ class GoogleCalendarService():
return service return service
@staticmethod @staticmethod
def from_srv_acc_file(service_account_file): def from_srv_acc_file(service_account_file: str):
"""make service Resource from service account filename (authorize) """make service Resource from service account filename (authorize)
Returns:
service Resource
""" """
scopes = ['https://www.googleapis.com/auth/calendar'] scopes = ['https://www.googleapis.com/auth/calendar']
@ -47,18 +43,15 @@ class GoogleCalendarService():
return service return service
@staticmethod @staticmethod
def from_config(config=None): def from_config(config: Optional[Dict[str, Optional[str]]] = None):
"""make service Resource from config dict """make service Resource from config dict
Arguments: Arguments:
config -- dict() config with keys: config -- config with keys:
(optional) service_account: - service account filename (optional) service_account: - service account filename
if key not in dict then default credentials will be used if key not in dict then default credentials will be used
( https://developers.google.com/identity/protocols/application-default-credentials ) ( https://developers.google.com/identity/protocols/application-default-credentials )
-- None: default credentials will be used -- None: default credentials will be used
Returns:
service Resource
""" """
if config is not None and 'service_account' in config: if config is not None and 'service_account' in config:
@ -69,7 +62,7 @@ class GoogleCalendarService():
return service return service
def select_event_key(event): def select_event_key(event: Dict[str, Any]) -> Optional[str]:
"""select event key for logging """select event key for logging
Arguments: Arguments:
@ -87,17 +80,17 @@ def select_event_key(event):
return key return key
class GoogleCalendar(): class GoogleCalendar:
"""class to interact with calendar on google """class to interact with calendar on google
""" """
logger = logging.getLogger('GoogleCalendar') logger = logging.getLogger('GoogleCalendar')
def __init__(self, service, calendarId): def __init__(self, service: discovery.Resource, calendarId: Optional[str]):
self.service = service self.service: discovery.Resource = service
self.calendarId = calendarId self.calendarId: str = calendarId
def _make_request_callback(self, action, events_by_req): def _make_request_callback(self, action: str, events_by_req: List[Dict[str, Any]]) -> Callable:
"""make callback for log result of batch request """make callback for log result of batch request
Arguments: Arguments:
@ -126,7 +119,7 @@ class GoogleCalendar():
action, key, event.get(key)) action, key, event.get(key))
return callback return callback
def list_events_from(self, start): def list_events_from(self, start: datetime) -> List[Dict[str, Any]]:
""" list events from calendar, where start date >= start """ list events from calendar, where start date >= start
""" """
fields = 'nextPageToken,items(id,iCalUID,updated)' fields = 'nextPageToken,items(id,iCalUID,updated)'
@ -148,7 +141,7 @@ class GoogleCalendar():
self.logger.info('%d events listed', len(events)) self.logger.info('%d events listed', len(events))
return events return events
def find_exists(self, events): def find_exists(self, events: List) -> Tuple[List[Tuple[Dict[str, Any], Dict[str, Any]]], List[Dict[str, Any]]]:
""" find existing events from list, by 'iCalUID' field """ find existing events from list, by 'iCalUID' field
Arguments: Arguments:
@ -166,16 +159,16 @@ class GoogleCalendar():
def list_callback(request_id, response, exception): def list_callback(request_id, response, exception):
found = False found = False
event = events_by_req[int(request_id)] cur_event = events_by_req[int(request_id)]
if exception is None: if exception is None:
found = ([] != response['items']) found = ([] != response['items'])
else: else:
self.logger.error( self.logger.error(
'exception %s, while listing event with UID: %s', 'exception %s, while listing event with UID: %s',
str(exception), event['iCalUID']) str(exception), cur_event['iCalUID'])
if found: if found:
exists.append( exists.append(
(event, response['items'][0])) (cur_event, response['items'][0]))
else: else:
not_found.append(events_by_req[int(request_id)]) not_found.append(events_by_req[int(request_id)])
@ -196,7 +189,7 @@ class GoogleCalendar():
len(exists), len(not_found)) len(exists), len(not_found))
return exists, not_found return exists, not_found
def insert_events(self, events): def insert_events(self, events: List[Dict[str, Any]]):
""" insert list of events """ insert list of events
Arguments: Arguments:
@ -218,7 +211,7 @@ class GoogleCalendar():
i += 1 i += 1
batch.execute() batch.execute()
def patch_events(self, event_tuples): def patch_events(self, event_tuples: List[Tuple[Dict[str, Any], Dict[str, Any]]]):
""" patch (update) events """ patch (update) events
Arguments: Arguments:
@ -241,7 +234,7 @@ class GoogleCalendar():
i += 1 i += 1
batch.execute() batch.execute()
def update_events(self, event_tuples): def update_events(self, event_tuples: List[Tuple[Dict[str, Any], Dict[str, Any]]]):
""" update events """ update events
Arguments: Arguments:
@ -264,7 +257,7 @@ class GoogleCalendar():
i += 1 i += 1
batch.execute() batch.execute()
def delete_events(self, events): def delete_events(self, events: List[Dict[str, Any]]):
""" delete events """ delete events
Arguments: Arguments:
@ -284,7 +277,7 @@ class GoogleCalendar():
i += 1 i += 1
batch.execute() batch.execute()
def create(self, summary, timeZone=None): def create(self, summary: str, timeZone: Optional[str] = None) -> Any:
"""create calendar """create calendar
Arguments: Arguments:
@ -328,7 +321,7 @@ class GoogleCalendar():
body=rule_public body=rule_public
).execute() ).execute()
def add_owner(self, email): def add_owner(self, email: str):
"""add calendar owner by email """add calendar owner by email
Arguments: Arguments:

View File

@ -1,12 +1,14 @@
import datetime import datetime
import logging import logging
from typing import Union, Dict, Any, Callable, Optional, List
from icalendar import Calendar, Event from icalendar import Calendar, Event
from pytz import utc from pytz import utc
def format_datetime_utc(value): def format_datetime_utc(value: Union[datetime.date, datetime.datetime]) -> str:
"""utc datetime as string from date or datetime value """utc datetime as string from date or datetime value
Arguments: Arguments:
value -- date or datetime value value -- date or datetime value
@ -23,20 +25,23 @@ def format_datetime_utc(value):
).replace(tzinfo=None).isoformat() + 'Z' ).replace(tzinfo=None).isoformat() + 'Z'
def gcal_date_or_dateTime(value, check_value=None): def gcal_date_or_dateTime(value: Union[datetime.date, datetime.datetime],
check_value: Union[datetime.date, datetime.datetime, None] = None)\
-> Dict[str, str]:
"""date or dateTime to gcal (start or end dict) """date or dateTime to gcal (start or end dict)
Arguments: Arguments:
value -- date or datetime value value: date or datetime
check_value - date or datetime to choise result type (if not None) check_value: optional for choose result type
Returns: Returns:
dict { 'date': ... } or { 'dateTime': ... } { 'date': ... } or { 'dateTime': ... }
""" """
if check_value is None: if check_value is None:
check_value = value check_value = value
result = {} result: Dict[str, str] = {}
if isinstance(check_value, datetime.datetime): if isinstance(check_value, datetime.datetime):
result['dateTime'] = format_datetime_utc(value) result['dateTime'] = format_datetime_utc(value)
else: else:
@ -52,7 +57,7 @@ class EventConverter(Event):
( https://developers.google.com/calendar/v3/reference/events#resource-representations ) ( https://developers.google.com/calendar/v3/reference/events#resource-representations )
""" """
def _str_prop(self, prop): def _str_prop(self, prop: str) -> str:
"""decoded string property """decoded string property
Arguments: Arguments:
@ -64,7 +69,7 @@ class EventConverter(Event):
return self.decoded(prop).decode(encoding='utf-8') return self.decoded(prop).decode(encoding='utf-8')
def _datetime_str_prop(self, prop): def _datetime_str_prop(self, prop: str) -> str:
"""utc datetime as string from property """utc datetime as string from property
Arguments: Arguments:
@ -76,7 +81,7 @@ class EventConverter(Event):
return format_datetime_utc(self.decoded(prop)) return format_datetime_utc(self.decoded(prop))
def _gcal_start(self): def _gcal_start(self) -> Dict[str, str]:
""" event start dict from icalendar event """ event start dict from icalendar event
Raises: Raises:
@ -89,7 +94,7 @@ class EventConverter(Event):
value = self.decoded('DTSTART') value = self.decoded('DTSTART')
return gcal_date_or_dateTime(value) return gcal_date_or_dateTime(value)
def _gcal_end(self): def _gcal_end(self) -> Dict[str, str]:
"""event end dict from icalendar event """event end dict from icalendar event
Raises: Raises:
@ -112,7 +117,9 @@ class EventConverter(Event):
raise ValueError('no DTEND or DURATION') raise ValueError('no DTEND or DURATION')
return result return result
def _put_to_gcal(self, gcal_event, prop, func, ics_prop=None): def _put_to_gcal(self, gcal_event: Dict[str, Any],
prop: str, func: Callable[[str], str],
ics_prop: Optional[str] = None):
"""get property from ical event if exist, and put to gcal event """get property from ical event if exist, and put to gcal event
Arguments: Arguments:
@ -127,7 +134,7 @@ class EventConverter(Event):
if ics_prop in self: if ics_prop in self:
gcal_event[prop] = func(ics_prop) gcal_event[prop] = func(ics_prop)
def to_gcal(self): def to_gcal(self) -> Dict[str, Any]:
"""Convert """Convert
Returns: Returns:
@ -135,12 +142,11 @@ class EventConverter(Event):
""" """
event = { event = {
'iCalUID': self._str_prop('UID') 'iCalUID': self._str_prop('UID'),
'start': self._gcal_start(),
'end': self._gcal_end()
} }
event['start'] = self._gcal_start()
event['end'] = self._gcal_end()
self._put_to_gcal(event, 'summary', self._str_prop) self._put_to_gcal(event, 'summary', self._str_prop)
self._put_to_gcal(event, 'description', self._str_prop) self._put_to_gcal(event, 'description', self._str_prop)
self._put_to_gcal(event, 'location', self._str_prop) self._put_to_gcal(event, 'location', self._str_prop)
@ -155,28 +161,28 @@ class EventConverter(Event):
return event return event
class CalendarConverter(): class CalendarConverter:
"""Convert icalendar events to google calendar resources """Convert icalendar events to google calendar resources
""" """
logger = logging.getLogger('CalendarConverter') logger = logging.getLogger('CalendarConverter')
def __init__(self, calendar=None): def __init__(self, calendar: Optional[Calendar] = None):
self.calendar = calendar self.calendar: Optional[Calendar] = calendar
def load(self, filename): def load(self, filename: str):
""" load calendar from ics file """ load calendar from ics file
""" """
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
self.calendar = Calendar.from_ical(f.read()) self.calendar = Calendar.from_ical(f.read())
self.logger.info('%s loaded', filename) self.logger.info('%s loaded', filename)
def loads(self, string): def loads(self, string: str):
""" load calendar from ics string """ load calendar from ics string
""" """
self.calendar = Calendar.from_ical(string) self.calendar = Calendar.from_ical(string)
def events_to_gcal(self): def events_to_gcal(self) -> List[Dict[str, Any]]:
"""Convert events to google calendar resources """Convert events to google calendar resources
""" """

View File

@ -1,5 +1,6 @@
import argparse import argparse
import logging.config import logging.config
from typing import Optional, Dict, Any
import yaml import yaml
@ -70,7 +71,7 @@ def parse_args():
return args return args
def load_config(): def load_config() -> Optional[Dict[str, Any]]:
result = None result = None
try: try:
with open('config.yml', 'r', encoding='utf-8') as f: with open('config.yml', 'r', encoding='utf-8') as f:
@ -81,7 +82,7 @@ def load_config():
return result return result
def list_calendars(service, show_hidden, show_deleted): def list_calendars(service, show_hidden: bool, show_deleted: bool) -> None:
fields = 'nextPageToken,items(id,summary)' fields = 'nextPageToken,items(id,summary)'
calendars = [] calendars = []
page_token = None page_token = None
@ -100,7 +101,7 @@ def list_calendars(service, show_hidden, show_deleted):
print('{summary}: {id}'.format_map(calendar)) print('{summary}: {id}'.format_map(calendar))
def create_calendar(service, summary, timezone, public): def create_calendar(service, summary: str, timezone: str, public: bool) -> None:
calendar = GoogleCalendar(service, None) calendar = GoogleCalendar(service, None)
calendar.create(summary, timezone) calendar.create(summary, timezone)
if public: if public:
@ -108,33 +109,33 @@ def create_calendar(service, summary, timezone, public):
print('{}: {}'.format(summary, calendar.calendarId)) print('{}: {}'.format(summary, calendar.calendarId))
def add_owner(service, id, owner_email): def add_owner(service, calendar_id: str, owner_email: str) -> None:
calendar = GoogleCalendar(service, id) calendar = GoogleCalendar(service, calendar_id)
calendar.add_owner(owner_email) calendar.add_owner(owner_email)
print('to {} added owner: {}'.format(id, owner_email)) print('to {} added owner: {}'.format(calendar_id, owner_email))
def remove_calendar(service, id): def remove_calendar(service, calendar_id: str) -> None:
calendar = GoogleCalendar(service, id) calendar = GoogleCalendar(service, calendar_id)
calendar.delete() calendar.delete()
print('removed: {}'.format(id)) print('removed: {}'.format(calendar_id))
def rename_calendar(service, id, summary): def rename_calendar(service, calendar_id: str, summary: str) -> None:
calendar = {'summary': summary} calendar = {'summary': summary}
service.calendars().patch(body=calendar, calendarId=id).execute() service.calendars().patch(body=calendar, calendarId=calendar_id).execute()
print('{}: {}'.format(summary, id)) print('{}: {}'.format(summary, calendar_id))
def get_calendar_property(service, id, property): def get_calendar_property(service, calendar_id: str, property_name: str) -> None:
response = service.calendarList().get(calendarId=id, response = service.calendarList().get(calendarId=calendar_id,
fields=property).execute() fields=property_name).execute()
print(response.get(property)) print(response.get(property_name))
def set_calendar_property(service, id, property, property_value): def set_calendar_property(service, calendar_id: str, property_name: str, property_value: str) -> None:
body = {property: property_value} body = {property_name: property_value}
response = service.calendarList().patch(body=body, calendarId=id).execute() response = service.calendarList().patch(body=body, calendarId=calendar_id).execute()
print(response) print(response)

View File

@ -1,22 +1,33 @@
import datetime import datetime
import dateutil.parser
import logging import logging
import operator import operator
from typing import List, Any, Dict, Set, Tuple, Union, Callable
import dateutil.parser
from pytz import utc from pytz import utc
from .gcal import GoogleCalendar
from .ical import CalendarConverter
class CalendarSync():
class CalendarSync:
"""class for syncronize calendar with google """class for syncronize calendar with google
""" """
logger = logging.getLogger('CalendarSync') logger = logging.getLogger('CalendarSync')
def __init__(self, gcalendar, converter): def __init__(self, gcalendar: GoogleCalendar, converter: CalendarConverter):
self.gcalendar = gcalendar self.gcalendar: GoogleCalendar = gcalendar
self.converter = converter self.converter: CalendarConverter = converter
self.to_insert: List[Dict[str, Any]] = []
self.to_update: List[Tuple[Dict[str, Any], Dict[str, Any]]] = []
self.to_delete: List[Dict[str, Any]] = []
@staticmethod @staticmethod
def _events_list_compare(items_src, items_dst, key='iCalUID'): def _events_list_compare(items_src: List[Dict[str, Any]],
items_dst: List[Dict[str, Any]],
key: str = 'iCalUID') \
-> Tuple[List[Dict[str, Any]], List[Tuple[Dict[str, Any], Dict[str, Any]]], List[Dict[str, Any]]]:
""" compare list of events by key """ compare list of events by key
Arguments: Arguments:
@ -30,16 +41,18 @@ class CalendarSync():
items_to_delete) items_to_delete)
""" """
def get_key(item): return item[key] def get_key(item: Dict[str, Any]) -> str: return item[key]
keys_src = set(map(get_key, items_src)) keys_src: Set[str] = set(map(get_key, items_src))
keys_dst = set(map(get_key, items_dst)) keys_dst: Set[str] = set(map(get_key, items_dst))
keys_to_insert = keys_src - keys_dst keys_to_insert = keys_src - keys_dst
keys_to_update = keys_src & keys_dst keys_to_update = keys_src & keys_dst
keys_to_delete = keys_dst - keys_src keys_to_delete = keys_dst - keys_src
def items_by_keys(items, key_name, keys): def items_by_keys(items: List[Dict[str, Any]],
key_name: str,
keys: Set[str]) -> List[Dict[str, Any]]:
return list(filter(lambda item: item[key_name] in keys, items)) return list(filter(lambda item: item[key_name] in keys, items))
items_to_insert = items_by_keys(items_src, key, keys_to_insert) items_to_insert = items_by_keys(items_src, key, keys_to_insert)
@ -57,7 +70,7 @@ class CalendarSync():
""" filter 'to_update' events by 'updated' datetime """ filter 'to_update' events by 'updated' datetime
""" """
def filter_updated(event_tuple): def filter_updated(event_tuple: Tuple[Dict[str, Any], Dict[str, Any]]) -> bool:
new, old = event_tuple new, old = event_tuple
new_date = dateutil.parser.parse(new['updated']) new_date = dateutil.parser.parse(new['updated'])
old_date = dateutil.parser.parse(old['updated']) old_date = dateutil.parser.parse(old['updated'])
@ -66,7 +79,10 @@ class CalendarSync():
self.to_update = list(filter(filter_updated, self.to_update)) self.to_update = list(filter(filter_updated, self.to_update))
@staticmethod @staticmethod
def _filter_events_by_date(events, date, op): def _filter_events_by_date(events: List[Dict[str, Any]],
date: Union[datetime.date, datetime.datetime],
op: Callable[[Union[datetime.date, datetime.datetime],
Union[datetime.date, datetime.datetime]], bool]) -> List[Dict[str, Any]]:
""" filter events by start datetime """ filter events by start datetime
Arguments: Arguments:
@ -78,10 +94,10 @@ class CalendarSync():
list of filtred events list of filtred events
""" """
def filter_by_date(event): def filter_by_date(event: Dict[str, Any]) -> bool:
date_cmp = date date_cmp = date
event_start = event['start'] event_start: Dict[str, str] = event['start']
event_date = None event_date: Union[datetime.date, datetime.datetime, str, None] = None
compare_dates = False compare_dates = False
if 'date' in event_start: if 'date' in event_start:
@ -101,7 +117,7 @@ class CalendarSync():
return list(filter(filter_by_date, events)) return list(filter(filter_by_date, events))
@staticmethod @staticmethod
def _tz_aware_datetime(date): def _tz_aware_datetime(date: Union[datetime.date, datetime.datetime]) -> datetime.datetime:
"""make tz aware datetime from datetime/date (utc if no tzinfo) """make tz aware datetime from datetime/date (utc if no tzinfo)
Arguments: Arguments:
@ -117,7 +133,7 @@ class CalendarSync():
date = date.replace(tzinfo=utc) date = date.replace(tzinfo=utc)
return date return date
def prepare_sync(self, start_date): def prepare_sync(self, start_date: Union[datetime.date, datetime.datetime]) -> None:
"""prepare sync lists by comparsion of events """prepare sync lists by comparsion of events
Arguments: Arguments:
@ -135,28 +151,20 @@ class CalendarSync():
events_src_past = CalendarSync._filter_events_by_date( events_src_past = CalendarSync._filter_events_by_date(
events_src, start_date, operator.lt) events_src, start_date, operator.lt)
events_src = None
# first events comparsion # first events comparsion
self.to_insert, self.to_update, self.to_delete = CalendarSync._events_list_compare( self.to_insert, self.to_update, self.to_delete = CalendarSync._events_list_compare(
events_src_pending, events_dst) events_src_pending, events_dst)
events_src_pending, events_dst = None, None
# find in events 'to_delete' past events from source, for update (move to past) # find in events 'to_delete' past events from source, for update (move to past)
_, add_to_update, self.to_delete = CalendarSync._events_list_compare( _, add_to_update, self.to_delete = CalendarSync._events_list_compare(
events_src_past, self.to_delete) events_src_past, self.to_delete)
self.to_update.extend(add_to_update) self.to_update.extend(add_to_update)
events_src_past = None
# find if events 'to_insert' exists in gcalendar, for update them # find if events 'to_insert' exists in gcalendar, for update them
add_to_update, self.to_insert = self.gcalendar.find_exists( add_to_update, self.to_insert = self.gcalendar.find_exists(
self.to_insert) self.to_insert)
self.to_update.extend(add_to_update) self.to_update.extend(add_to_update)
add_to_update = None
# exclude outdated events from 'to_update' list, by 'updated' field # exclude outdated events from 'to_update' list, by 'updated' field
self._filter_events_to_update() self._filter_events_to_update()
@ -167,14 +175,21 @@ class CalendarSync():
len(self.to_delete) len(self.to_delete)
) )
def apply(self): def clear(self) -> None:
"""apply sync (insert, update, delete), using prepared lists of events """ clear prepared sync lists (insert, update, delete)
"""
self.to_insert.clear()
self.to_update.clear()
self.to_delete.clear()
def apply(self) -> None:
""" apply sync (insert, update, delete), using prepared lists of events
""" """
self.gcalendar.insert_events(self.to_insert) self.gcalendar.insert_events(self.to_insert)
self.gcalendar.update_events(self.to_update) self.gcalendar.update_events(self.to_update)
self.gcalendar.delete_events(self.to_delete) self.gcalendar.delete_events(self.to_delete)
self.logger.info('sync done') self.clear()
self.to_insert, self.to_update, self.to_delete = [], [], [] self.logger.info('sync done')

View File

@ -1,3 +1,5 @@
from typing import Dict, Any
import yaml import yaml
import dateutil.parser import dateutil.parser
@ -12,14 +14,13 @@ from . import (
) )
def load_config(): def load_config() -> Dict[str, Any]:
with open('config.yml', 'r', encoding='utf-8') as f: with open('config.yml', 'r', encoding='utf-8') as f:
result = yaml.safe_load(f) result = yaml.safe_load(f)
return result return result
def get_start_date(date_str): def get_start_date(date_str: str) -> datetime.datetime:
result = datetime.datetime(1, 1, 1)
if 'now' == date_str: if 'now' == date_str:
result = datetime.datetime.utcnow() result = datetime.datetime.utcnow()
else: else:
@ -33,8 +34,8 @@ def main():
if 'logging' in config: if 'logging' in config:
logging.config.dictConfig(config['logging']) logging.config.dictConfig(config['logging'])
calendarId = config['calendar']['google_id'] calendarId: str = config['calendar']['google_id']
ics_filepath = config['calendar']['source'] ics_filepath: str = config['calendar']['source']
start = get_start_date(config['start_from']) start = get_start_date(config['start_from'])

View File

@ -1,3 +1,5 @@
from typing import Tuple
import pytest import pytest
from sync_ics2gcal import CalendarConverter from sync_ics2gcal import CalendarConverter
@ -26,11 +28,11 @@ LAST-MODIFIED:20180326T120235Z
""" """
def ics_test_cal(content): def ics_test_cal(content: str) -> str:
return "BEGIN:VCALENDAR\r\n{}END:VCALENDAR\r\n".format(content) return "BEGIN:VCALENDAR\r\n{}END:VCALENDAR\r\n".format(content)
def ics_test_event(content): def ics_test_event(content: str) -> str:
return ics_test_cal("BEGIN:VEVENT\r\n{}END:VEVENT\r\n".format(content)) return ics_test_cal("BEGIN:VEVENT\r\n{}END:VEVENT\r\n".format(content))
@ -68,7 +70,7 @@ def param_events_start_end(request):
return request.param return request.param
def test_event_start_end(param_events_start_end): def test_event_start_end(param_events_start_end: Tuple[str, str, str, str]):
(date_type, ics_str, start, end) = param_events_start_end (date_type, ics_str, start, end) = param_events_start_end
converter = CalendarConverter() converter = CalendarConverter()
converter.loads(ics_str) converter.loads(ics_str)

View File

@ -3,6 +3,7 @@ import hashlib
import operator import operator
from copy import deepcopy from copy import deepcopy
from random import shuffle from random import shuffle
from typing import Union, List, Dict, Optional
import dateutil.parser import dateutil.parser
import pytest import pytest
@ -11,7 +12,7 @@ from pytz import timezone, utc
from sync_ics2gcal import CalendarSync from sync_ics2gcal import CalendarSync
def sha1(string): def sha1(string: Union[str, bytes]) -> str:
if isinstance(string, str): if isinstance(string, str):
string = string.encode('utf8') string = string.encode('utf8')
h = hashlib.sha1() h = hashlib.sha1()
@ -19,55 +20,57 @@ def sha1(string):
return h.hexdigest() return h.hexdigest()
def gen_events(start, stop, start_time, no_time=False): def gen_events(start: int,
stop: int,
start_time: Union[datetime.datetime, datetime.date],
no_time: bool = False) -> List[Dict[str, Union[str, Dict[str, str]]]]:
if no_time: if no_time:
start_time = datetime.date( start_time = datetime.date(
start_time.year, start_time.month, start_time.day) start_time.year, start_time.month, start_time.day)
duration = datetime.date(1, 1, 2) - datetime.date(1, 1, 1) duration: datetime.timedelta = datetime.date(1, 1, 2) - datetime.date(1, 1, 1)
date_key = "date" date_key: str = "date"
suff = '' date_end: str = ''
else: else:
start_time = utc.normalize( start_time = utc.normalize(
start_time.astimezone(utc)).replace(tzinfo=None) start_time.astimezone(utc)).replace(tzinfo=None)
duration = datetime.datetime( duration: datetime.timedelta = datetime.datetime(1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1)
1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1) date_key: str = "dateTime"
date_key = "dateTime" date_end: str = 'Z'
suff = 'Z'
result = [] result: List[Dict[str, Union[str, Dict[str, str]]]] = []
for i in range(start, stop): for i in range(start, stop):
event_start = start_time + (duration * i) event_start = start_time + (duration * i)
event_end = event_start + duration event_end = event_start + duration
updated = event_start updated: Union[datetime.datetime, datetime.date] = event_start
if no_time: if no_time:
updated = datetime.datetime( updated = datetime.datetime(
updated.year, updated.month, updated.day, 0, 0, 0, 1, tzinfo=utc) updated.year, updated.month, updated.day, 0, 0, 0, 1, tzinfo=utc)
event = { event: Dict[str, Union[str, Dict[str, str]]] = {
'summary': 'test event __ {}'.format(i), 'summary': 'test event __ {}'.format(i),
'location': 'la la la {}'.format(i), 'location': 'la la la {}'.format(i),
'description': 'test TEST -- test event {}'.format(i), 'description': 'test TEST -- test event {}'.format(i),
"iCalUID": "{}@test.com".format(sha1("test - event {}".format(i))), "iCalUID": "{}@test.com".format(sha1("test - event {}".format(i))),
"updated": updated.isoformat() + 'Z', "updated": updated.isoformat() + 'Z',
"created": updated.isoformat() + 'Z' "created": updated.isoformat() + 'Z',
'start': {date_key: event_start.isoformat() + date_end},
'end': {date_key: event_end.isoformat() + date_end}
} }
event['start'] = {date_key: event_start.isoformat() + suff}
event['end'] = {date_key: event_end.isoformat() + suff}
result.append(event) result.append(event)
return result return result
def gen_list_to_compare(start, stop): def gen_list_to_compare(start: int, stop: int) -> List[Dict[str, str]]:
result = [] result: List[Dict[str, str]] = []
for i in range(start, stop): for i in range(start, stop):
result.append({'iCalUID': 'test{:06d}'.format(i)}) result.append({'iCalUID': 'test{:06d}'.format(i)})
return result return result
def get_start_date(event): def get_start_date(event: Dict[str, Union[str, Dict[str, str]]]) -> Union[datetime.datetime, datetime.date]:
event_start = event['start'] event_start: Dict[str, str] = event['start']
start_date = None start_date: Optional[str] = None
is_date = False is_date = False
if 'date' in event_start: if 'date' in event_start:
start_date = event_start['date'] start_date = event_start['date']
@ -113,7 +116,7 @@ def test_compare():
@pytest.mark.parametrize("no_time", [True, False], ids=['date', 'dateTime']) @pytest.mark.parametrize("no_time", [True, False], ids=['date', 'dateTime'])
def test_filter_events_by_date(no_time): def test_filter_events_by_date(no_time: bool):
msk = timezone('Europe/Moscow') msk = timezone('Europe/Moscow')
now = utc.localize(datetime.datetime.utcnow()) now = utc.localize(datetime.datetime.utcnow())
msk_now = msk.normalize(now.astimezone(msk)) msk_now = msk.normalize(now.astimezone(msk))