1
0
mirror of https://github.com/b4tman/sync_ics2gcal synced 2024-12-04 17:56:54 +00:00

Merge branch 'converter/pydantic' of https://github.com/b4tman/sync_ics2gcal into converter/pydantic

This commit is contained in:
Dmitry Belyaev 2021-10-21 09:58:14 +03:00
commit d2d43d02da
Signed by: b4tman
GPG Key ID: 41A00BF15EA7E5F3
3 changed files with 151 additions and 25 deletions

74
poetry.lock generated
View File

@ -20,6 +20,18 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
name = "autopep8"
version = "1.5.7"
description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
pycodestyle = ">=2.7.0"
toml = "*"
[[package]]
name = "cachetools"
version = "4.2.4"
@ -55,6 +67,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dataclasses"
version = "0.8"
description = "A backport of the dataclasses module for Python 3.6"
category = "main"
optional = false
python-versions = ">=3.6, <3.7"
[[package]]
name = "fire"
version = "0.4.0"
@ -293,6 +313,22 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pydantic"
version = "1.8.2"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
python-versions = ">=3.6.1"
[package.dependencies]
dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
typing-extensions = ">=3.7.4.3"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyflakes"
version = "2.3.1"
@ -415,7 +451,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
category = "main"
optional = false
python-versions = "*"
@ -454,8 +490,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6"
content-hash = "712c4284d2a0349b959dad1bfec496daa5020135d340ef3859b641d567276b33"
python-versions = ">=3.6.1"
content-hash = "9145569d3597a35a93b05b114ac7bcdba29e1b1fb215793b427b0e40fbe52bef"
[metadata.files]
atomicwrites = [
@ -466,6 +502,10 @@ attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
autopep8 = [
{file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"},
{file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"},
]
cachetools = [
{file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"},
{file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"},
@ -482,6 +522,10 @@ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
]
fire = [
{file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"},
]
@ -602,6 +646,30 @@ pycodestyle = [
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
]
pydantic = [
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
{file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
{file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
{file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
{file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
{file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
{file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
{file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
]
pyflakes = [
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},

View File

@ -19,17 +19,19 @@ classifiers = [
]
[tool.poetry.dependencies]
python = "^3.6"
python = ">=3.6.1"
google-auth = "2.2.1"
google-api-python-client = "2.23.0"
icalendar = "4.0.7"
pytz = "2021.1"
PyYAML = "5.4.1"
fire = "0.4.0"
pydantic = "^1.8.2"
[tool.poetry.dev-dependencies]
pytest = "^6.2.5"
flake8 = "^3.9.2"
autopep8 = "^1.5.7"
[tool.poetry.scripts]
sync-ics2gcal = "sync_ics2gcal.sync_calendar:main"

View File

@ -5,11 +5,56 @@ from typing import Union, Dict, Callable, Optional
from icalendar import Calendar, Event
from pytz import utc
import pydantic
from .gcal import EventData, EventList
DateDateTime = Union[datetime.date, datetime.datetime]
class GCal_DateDateTime(pydantic.BaseModel):
date: Optional[str] = pydantic.Field(default=None)
date_time: Optional[str] = pydantic.Field(alias='dateTime', default=None)
timezone: Optional[str] = pydantic.Field(alias='timeZone', default=None)
@pydantic.root_validator(allow_reuse=True)
def check_only_date_or_datetime(cls, values):
date = values.get('date', None)
date_time = values.get('date_time', None)
assert (date is None) != (date_time is None), \
'only date or date_time must be provided'
return values
@classmethod
def create_from(cls, value: DateDateTime) -> 'GCal_DateDateTime':
key: str = 'date'
str_value: str = ''
if type(value) is datetime.datetime:
key = 'date_time'
str_value = format_datetime_utc(value)
else:
str_value = value.isoformat()
return cls(**{key: str_value})
class Config:
allow_population_by_field_name = True
class GCal_Event(pydantic.BaseModel):
created: Optional[str] = None
updated: Optional[str] = None
summary: Optional[str] = None
description: Optional[str] = None
location: Optional[str] = None
start: GCal_DateDateTime
end: GCal_DateDateTime
transparency: Optional[str] = None
ical_uid: str = pydantic.Field(alias='iCalUID')
class Config:
allow_population_by_field_name = True
def format_datetime_utc(value: DateDateTime) -> str:
"""utc datetime as string from date or datetime value
@ -19,7 +64,7 @@ def format_datetime_utc(value: DateDateTime) -> str:
Returns:
utc datetime value as string in iso format
"""
if not isinstance(value, datetime.datetime):
if type(value) is datetime.date:
value = datetime.datetime(
value.year, value.month, value.day, tzinfo=utc)
value = value.replace(microsecond=1)
@ -85,7 +130,7 @@ class EventConverter(Event):
return format_datetime_utc(self.decoded(prop))
def _gcal_start(self) -> Dict[str, str]:
def _gcal_start(self) -> GCal_DateDateTime:
""" event start dict from icalendar event
Raises:
@ -96,9 +141,9 @@ class EventConverter(Event):
"""
value = self.decoded('DTSTART')
return gcal_date_or_dateTime(value)
return GCal_DateDateTime.create_from(value)
def _gcal_end(self) -> Dict[str, str]:
def _gcal_end(self) -> GCal_DateDateTime:
"""event end dict from icalendar event
Raises:
@ -110,13 +155,17 @@ class EventConverter(Event):
result = None
if 'DTEND' in self:
value = self.decoded('DTEND')
result = gcal_date_or_dateTime(value)
result = GCal_DateDateTime.create_from(value)
elif 'DURATION' in self:
start_val = self.decoded('DTSTART')
duration = self.decoded('DURATION')
end_val = start_val + duration
if type(start_val) is datetime.date:
if type(end_val) is datetime.datetime:
end_val = datetime.date(
end_val.year, end_val.month, end_val.day)
result = gcal_date_or_dateTime(end_val, check_value=start_val)
result = GCal_DateDateTime.create_from(end_val)
else:
raise ValueError('no DTEND or DURATION')
return result
@ -138,6 +187,18 @@ class EventConverter(Event):
if ics_prop in self:
gcal_event[prop] = func(ics_prop)
def _get_prop(self, prop: str, func: Callable[[str], str]):
"""get property from ical event if exist else None
Arguments:
prop -- property name
func -- function to convert
"""
if prop not in self:
return None
return func(prop)
def to_gcal(self) -> EventData:
"""Convert
@ -145,24 +206,19 @@ class EventConverter(Event):
dict - google calendar#event resource
"""
event = {
'iCalUID': self._str_prop('UID'),
kwargs = {
'ical_uid': self._str_prop('UID'),
'start': self._gcal_start(),
'end': self._gcal_end()
'end': self._gcal_end(),
'summary': self._get_prop('SUMMARY', self._str_prop),
'description': self._get_prop('DESCRIPTION', self._str_prop),
'location': self._get_prop('LOCATION', self._str_prop),
'created': self._get_prop('CREATED', self._datetime_str_prop),
'updated': self._get_prop('LAST-MODIFIED', self._datetime_str_prop),
'transparency': self._get_prop('TRANSP', lambda prop: self._str_prop(prop).lower()),
}
self._put_to_gcal(event, 'summary', 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, 'created', self._datetime_str_prop)
self._put_to_gcal(
event, 'updated', self._datetime_str_prop, 'LAST-MODIFIED')
self._put_to_gcal(
event,
'transparency',
lambda prop: self._str_prop(prop).lower(), 'TRANSP')
return event
return GCal_Event(**kwargs).dict(by_alias=True, exclude_defaults=True)
class CalendarConverter: