1
0
mirror of https://github.com/b4tman/sync_ics2gcal synced 2026-02-04 15:25:04 +00:00

5 Commits

Author SHA1 Message Date
d2d43d02da Merge branch 'converter/pydantic' of https://github.com/b4tman/sync_ics2gcal into converter/pydantic 2021-10-21 09:58:14 +03:00
2e114db5c9 + bench_converter 2021-10-21 09:57:17 +03:00
15951ba200 try pydantic for converter
---
x2 slower:
before: best: _82001700 ns,     avg: _85379966.2 ns,    median: _84408700.0 ns
after__: best: 162860900 ns,     avg: 175015097.0 ns,    median: 171212750.0 ns
2021-10-17 17:10:44 +03:00
d03e5691ee add pydantic 2021-10-17 17:07:36 +03:00
3686bc29ee + bench_converter 2021-10-15 14:53:20 +03:00
17 changed files with 1330 additions and 1305 deletions

View File

@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@@ -17,16 +17,21 @@ jobs:
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
python-version: ['3.7', '3.8', '3.9', '3.10'] python-version: [3.6, 3.7, 3.8, 3.9]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Upgrade pip - name: Upgrade pip
run: python -m pip install --upgrade pip run: python -m pip install --upgrade pip
- name: Load cached Poetry installation
uses: actions/cache@v2
with:
path: ~/.local
key: poetry-0
- name: Install Poetry - name: Install Poetry
uses: snok/install-poetry@v1 uses: snok/install-poetry@v1
- name: Install deps - name: Install deps

View File

@@ -8,13 +8,18 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.x' python-version: '3.x'
- name: Upgrade pip - name: Upgrade pip
run: python -m pip install --upgrade pip run: python -m pip install --upgrade pip
- name: Load cached Poetry installation
uses: actions/cache@v2
with:
path: ~/.local
key: poetry-0
- name: Install Poetry - name: Install Poetry
uses: snok/install-poetry@v1 uses: snok/install-poetry@v1
- name: Install deps - name: Install deps

21
.travis.yml Normal file
View File

@@ -0,0 +1,21 @@
language: python
os: linux
dist: focal
python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
before_install:
- pip install poetry
install:
- poetry install
script:
# stop the build if there are Python syntax errors or undefined names
- poetry run flake8 sync_ics2gcal --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- poetry run flake8 sync_ics2gcal --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# run tests
- poetry run pytest -v

View File

@@ -1,6 +1,7 @@
# sync_ics2gcal # sync_ics2gcal
[![PyPI version](https://badge.fury.io/py/sync-ics2gcal.svg)](https://badge.fury.io/py/sync-ics2gcal) [![PyPI version](https://badge.fury.io/py/sync-ics2gcal.svg)](https://badge.fury.io/py/sync-ics2gcal)
[![Build Status](https://travis-ci.org/b4tman/sync_ics2gcal.svg?branch=master)](https://travis-ci.org/b4tman/sync_ics2gcal)
![Python package status](https://github.com/b4tman/sync_ics2gcal/workflows/Python%20package/badge.svg) ![Python package status](https://github.com/b4tman/sync_ics2gcal/workflows/Python%20package/badge.svg)
Python scripts for sync .ics file with Google calendar Python scripts for sync .ics file with Google calendar

547
poetry.lock generated
View File

@@ -8,48 +8,37 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "21.4.0" version = "21.2.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras] [package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 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", "cloudpickle"] 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", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]] [[package]]
name = "black" name = "autopep8"
version = "22.3.0" version = "1.5.7"
description = "The uncompromising code formatter." description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6.2" python-versions = "*"
[package.dependencies] [package.dependencies]
click = ">=8.0.0" pycodestyle = ">=2.7.0"
mypy-extensions = ">=0.4.3" toml = "*"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "cachetools" name = "cachetools"
version = "5.0.0" version = "4.2.4"
description = "Extensible memoizing collections and decorators" description = "Extensible memoizing collections and decorators"
category = "main" category = "main"
optional = false optional = false
python-versions = "~=3.7" python-versions = "~=3.5"
[[package]] [[package]]
name = "certifi" name = "certifi"
@@ -61,7 +50,7 @@ python-versions = "*"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.0.12" version = "2.0.6"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main" category = "main"
optional = false optional = false
@@ -70,18 +59,6 @@ python-versions = ">=3.5.0"
[package.extras] [package.extras]
unicode_backport = ["unicodedata2"] unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.0.4"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.4" version = "0.4.4"
@@ -90,6 +67,14 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 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]] [[package]]
name = "fire" name = "fire"
version = "0.4.0" version = "0.4.0"
@@ -104,21 +89,21 @@ termcolor = "*"
[[package]] [[package]]
name = "flake8" name = "flake8"
version = "4.0.1" version = "3.9.2"
description = "the modular source code checker: pep8 pyflakes and co" description = "the modular source code checker: pep8 pyflakes and co"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies] [package.dependencies]
importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
mccabe = ">=0.6.0,<0.7.0" mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.8.0,<2.9.0" pycodestyle = ">=2.7.0,<2.8.0"
pyflakes = ">=2.4.0,<2.5.0" pyflakes = ">=2.3.0,<2.4.0"
[[package]] [[package]]
name = "google-api-core" name = "google-api-core"
version = "2.5.0" version = "2.1.0"
description = "Google API client core library" description = "Google API client core library"
category = "main" category = "main"
optional = false optional = false
@@ -126,46 +111,45 @@ python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
google-auth = ">=1.25.0,<3.0dev" google-auth = ">=1.25.0,<3.0dev"
googleapis-common-protos = ">=1.52.0,<2.0dev" googleapis-common-protos = ">=1.6.0,<2.0dev"
protobuf = ">=3.12.0" protobuf = ">=3.12.0"
requests = ">=2.18.0,<3.0.0dev" requests = ">=2.18.0,<3.0.0dev"
[package.extras] [package.extras]
grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] grpc = ["grpcio (>=1.33.2,<2.0dev)"]
grpcgcp = ["grpcio-gcp (>=0.2.2)"] grpcgcp = ["grpcio-gcp (>=0.2.2)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
[[package]] [[package]]
name = "google-api-python-client" name = "google-api-python-client"
version = "2.49.0" version = "2.23.0"
description = "Google API Client Library for Python" description = "Google API Client Library for Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev" google-api-core = ">=1.21.0,<3.0.0dev"
google-auth = ">=1.16.0,<3.0.0dev" google-auth = ">=1.16.0,<3.0.0dev"
google-auth-httplib2 = ">=0.1.0" google-auth-httplib2 = ">=0.1.0"
httplib2 = ">=0.15.0,<1dev" httplib2 = ">=0.15.0,<1dev"
uritemplate = ">=3.0.1,<5" uritemplate = ">=3.0.0,<4dev"
[[package]] [[package]]
name = "google-auth" name = "google-auth"
version = "2.6.6" version = "2.2.1"
description = "Google Authentication Library" description = "Google Authentication Library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" python-versions = ">= 3.6"
[package.dependencies] [package.dependencies]
cachetools = ">=2.0.0,<6.0" cachetools = ">=2.0.0,<5.0"
pyasn1-modules = ">=0.2.1" pyasn1-modules = ">=0.2.1"
rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} rsa = ">=3.1.4,<5"
six = ">=1.9.0"
[package.extras] [package.extras]
aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"] aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"]
pyopenssl = ["pyopenssl (>=20.0.0)"] pyopenssl = ["pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"] reauth = ["pyu2f (>=0.1.5)"]
@@ -184,7 +168,7 @@ six = "*"
[[package]] [[package]]
name = "googleapis-common-protos" name = "googleapis-common-protos"
version = "1.54.0" version = "1.53.0"
description = "Common protobufs used in Google APIs" description = "Common protobufs used in Google APIs"
category = "main" category = "main"
optional = false optional = false
@@ -198,18 +182,18 @@ grpc = ["grpcio (>=1.0.0)"]
[[package]] [[package]]
name = "httplib2" name = "httplib2"
version = "0.20.4" version = "0.20.1"
description = "A comprehensive HTTP client library." description = "A comprehensive HTTP client library."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} pyparsing = ">=2.4.2,<3"
[[package]] [[package]]
name = "icalendar" name = "icalendar"
version = "4.0.9" version = "4.0.7"
description = "iCalendar parser/generator" description = "iCalendar parser/generator"
category = "main" category = "main"
optional = false optional = false
@@ -221,7 +205,7 @@ pytz = "*"
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.3" version = "3.2"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "main" category = "main"
optional = false optional = false
@@ -229,7 +213,7 @@ python-versions = ">=3.5"
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "4.2.0" version = "4.8.1"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "dev" category = "dev"
optional = false optional = false
@@ -241,7 +225,8 @@ zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] perf = ["ipython"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
@@ -259,44 +244,16 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.3" version = "21.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" pyparsing = ">=2.0.2"
[[package]]
name = "pathspec"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
version = "2.5.1"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
@@ -315,7 +272,7 @@ testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "3.19.4" version = "3.18.1"
description = "Protocol Buffers" description = "Protocol Buffers"
category = "main" category = "main"
optional = false optional = false
@@ -323,11 +280,11 @@ python-versions = ">=3.5"
[[package]] [[package]]
name = "py" name = "py"
version = "1.11.0" version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities" description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pyasn1" name = "pyasn1"
@@ -350,15 +307,31 @@ pyasn1 = ">=0.4.6,<0.5.0"
[[package]] [[package]]
name = "pycodestyle" name = "pycodestyle"
version = "2.8.0" version = "2.7.0"
description = "Python style guide checker" description = "Python style guide checker"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 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]] [[package]]
name = "pyflakes" name = "pyflakes"
version = "2.4.0" version = "2.3.1"
description = "passive checker of Python programs" description = "passive checker of Python programs"
category = "dev" category = "dev"
optional = false optional = false
@@ -366,22 +339,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "3.0.7" version = "2.4.7"
description = "Python parsing module" description = "Python parsing module"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.1.2" version = "6.2.5"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
@@ -392,10 +362,10 @@ iniconfig = "*"
packaging = "*" packaging = "*"
pluggy = ">=0.12,<2.0" pluggy = ">=0.12,<2.0"
py = ">=1.8.2" py = ">=1.8.2"
tomli = ">=1.0.0" toml = "*"
[package.extras] [package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
@@ -410,7 +380,7 @@ six = ">=1.5"
[[package]] [[package]]
name = "pytz" name = "pytz"
version = "2022.1" version = "2021.1"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
category = "main" category = "main"
optional = false optional = false
@@ -418,15 +388,15 @@ python-versions = "*"
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0" version = "5.4.1"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.27.1" version = "2.26.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
category = "main" category = "main"
optional = false optional = false
@@ -444,11 +414,11 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "4.8" version = "4.7.2"
description = "Pure-Python RSA implementation" description = "Pure-Python RSA implementation"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6,<4" python-versions = ">=3.5, <4"
[package.dependencies] [package.dependencies]
pyasn1 = ">=0.1.3" pyasn1 = ">=0.1.3"
@@ -470,40 +440,32 @@ optional = false
python-versions = "*" python-versions = "*"
[[package]] [[package]]
name = "tomli" name = "toml"
version = "2.0.1" version = "0.10.2"
description = "A lil' TOML parser" description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typed-ast"
version = "1.5.2"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.1.1" version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.6+" description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = "*"
[[package]] [[package]]
name = "uritemplate" name = "uritemplate"
version = "4.1.1" version = "3.0.1"
description = "Implementation of RFC 6570 URI Templates" description = "URI templates"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.8" version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
optional = false optional = false
@@ -516,20 +478,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.7.0" version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.6"
[package.extras] [package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = ">=3.6.1"
content-hash = "98872c98b9b2ff9d51a549f5d93e030fd77e038c030dec9daa1b0bd3b924e38b" content-hash = "9145569d3597a35a93b05b114ac7bcdba29e1b1fb215793b427b0e40fbe52bef"
[metadata.files] [metadata.files]
atomicwrites = [ atomicwrites = [
@@ -537,96 +499,75 @@ atomicwrites = [
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
] ]
attrs = [ attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
] ]
black = [ autopep8 = [
{file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"},
{file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"},
{file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
{file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
{file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
{file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
{file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
{file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
{file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
{file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
{file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
{file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
{file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
{file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
{file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
{file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
{file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
{file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
{file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
{file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
{file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
{file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
{file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
] ]
cachetools = [ cachetools = [
{file = "cachetools-5.0.0-py3-none-any.whl", hash = "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4"}, {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"},
{file = "cachetools-5.0.0.tar.gz", hash = "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6"}, {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"},
] ]
certifi = [ certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
] ]
charset-normalizer = [ charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
]
click = [
{file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
] ]
colorama = [ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {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 = [ fire = [
{file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"}, {file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"},
] ]
flake8 = [ flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
] ]
google-api-core = [ google-api-core = [
{file = "google-api-core-2.5.0.tar.gz", hash = "sha256:f33863a6709651703b8b18b67093514838c79f2b04d02aa501203079f24b8018"}, {file = "google-api-core-2.1.0.tar.gz", hash = "sha256:5ec27b942b34d04559cbf3674430bb83fc3d74e7d32b8bbd31c4466e71740b83"},
{file = "google_api_core-2.5.0-py2.py3-none-any.whl", hash = "sha256:7d030edbd3a0e994d796e62716022752684e863a6df9864b6ca82a1616c2a5a6"}, {file = "google_api_core-2.1.0-py2.py3-none-any.whl", hash = "sha256:c344e1aacd8330527c5130bdfe03118d8859ce798bcf0e5d23770ab6873e0615"},
] ]
google-api-python-client = [ google-api-python-client = [
{file = "google-api-python-client-2.49.0.tar.gz", hash = "sha256:629bbde991ce2d9697c6da37f2416f7aeb01ba01505b166066a415b3c3ce1dfc"}, {file = "google-api-python-client-2.23.0.tar.gz", hash = "sha256:f117a595717fc384446f6235019e6a83fc9df821bd9d05dba7ff14aa96c70f52"},
{file = "google_api_python_client-2.49.0-py2.py3-none-any.whl", hash = "sha256:7e172b06abeff7170108596446f0c8e56a129a40ef29a802270a02c0b07e993d"}, {file = "google_api_python_client-2.23.0-py2.py3-none-any.whl", hash = "sha256:a7b364eff63ca75d827cfb241a0f8567157976e879046c1ff20ddf735bad618e"},
] ]
google-auth = [ google-auth = [
{file = "google-auth-2.6.6.tar.gz", hash = "sha256:1ba4938e032b73deb51e59c4656a00e0939cf0b1112575099f136babb4563312"}, {file = "google-auth-2.2.1.tar.gz", hash = "sha256:6dc8173abd50f25b6e62fc5b42802c96fc7cd9deb9bfeeb10a79f5606225cdf4"},
{file = "google_auth-2.6.6-py2.py3-none-any.whl", hash = "sha256:349ac49b18b01019453cc99c11c92ed772739778c92f184002b7ab3a5b7ac77d"}, {file = "google_auth-2.2.1-py2.py3-none-any.whl", hash = "sha256:2a92b485afed5292946b324e91fcbe03db277ee4cb64c998c6cfa66d4af01dee"},
] ]
google-auth-httplib2 = [ google-auth-httplib2 = [
{file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"},
{file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"},
] ]
googleapis-common-protos = [ googleapis-common-protos = [
{file = "googleapis-common-protos-1.54.0.tar.gz", hash = "sha256:a4031d6ec6c2b1b6dc3e0be7e10a1bd72fb0b18b07ef9be7b51f2c1004ce2437"}, {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"},
{file = "googleapis_common_protos-1.54.0-py2.py3-none-any.whl", hash = "sha256:e54345a2add15dc5e1a7891c27731ff347b4c33765d79b5ed7026a6c0c7cbcae"}, {file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"},
] ]
httplib2 = [ httplib2 = [
{file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"}, {file = "httplib2-0.20.1-py3-none-any.whl", hash = "sha256:8fa4dbf2fbf839b71f8c7837a831e00fcdc860feca99b8bda58ceae4bc53d185"},
{file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"}, {file = "httplib2-0.20.1.tar.gz", hash = "sha256:0efbcb8bfbfbc11578130d87d8afcc65c2274c6eb446e59fc674e4d7c972d327"},
] ]
icalendar = [ icalendar = [
{file = "icalendar-4.0.9-py2.py3-none-any.whl", hash = "sha256:cf1446ffdf1b6ad469451a8966cfa7694f5fac796ac6fc7cd93e28c51a637d2c"}, {file = "icalendar-4.0.7-py2.py3-none-any.whl", hash = "sha256:8c35be16c1d0581a276002af883297aeffa8116e366fdce4d5318e1424aa1903"},
{file = "icalendar-4.0.9.tar.gz", hash = "sha256:cc73fa9c848744843046228cb66ea86cd8c18d73a51b140f7c003f760b84a997"}, {file = "icalendar-4.0.7.tar.gz", hash = "sha256:0fc18d87f66e0b5da84fa731389496cfe18e4c21304e8f6713556b2e8724a7a4"},
] ]
idna = [ idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
] ]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
] ]
iniconfig = [ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@@ -636,57 +577,40 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
] ]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [ packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"},
{file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"},
] ]
pluggy = [ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
] ]
protobuf = [ protobuf = [
{file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, {file = "protobuf-3.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fa6d1049d5315566f55c04d0b50c0033415144f96a9d25c820dc542fe2bb7f45"},
{file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, {file = "protobuf-3.18.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e2790c580070cff2921b93d562539ae027064340151c50db6aaf94c33048cd"},
{file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, {file = "protobuf-3.18.1-cp36-cp36m-win32.whl", hash = "sha256:7e2f0677d68ecdd1cfda2abea65873f5bc7c3f5aae199404a3f5c1d1198c1a63"},
{file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, {file = "protobuf-3.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6f714f5de9d40b3bec90ede4a688cce52f637ccdc5403afcda1f67598f4fdcd7"},
{file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, {file = "protobuf-3.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a7be937c319146cc9f2626f0181e6809062c353e1fe449ecd0df374ba1036b2"},
{file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, {file = "protobuf-3.18.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:10544fc7ace885a882623083c24da5b14148c77563acddc3c58d66f6153c09cd"},
{file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, {file = "protobuf-3.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ee8b11e3eb2ed38f12137c3c132270a0b1dd509e317228ac47b67f21a583f1"},
{file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, {file = "protobuf-3.18.1-cp37-cp37m-win32.whl", hash = "sha256:c492c217d3f69f4d2d5619571e52ab98538edbf53caf67e53ea92bd0a3b5670f"},
{file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, {file = "protobuf-3.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3c1644f8a7f19b45c7a4c32278e2a55ae9e7e2f9e5f02d960a61f04a4890d3e6"},
{file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, {file = "protobuf-3.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9ac691f7b24e4371dcd3980e4f5d6c840a2010da37986203053fee995786ec5"},
{file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, {file = "protobuf-3.18.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:93bad12895d8b0ebc66b605c2ef1802311595f881aef032d9f13282b7550e6b2"},
{file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, {file = "protobuf-3.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0851b5b89191e1976d34fa2e8eb8659829dfb45252053224cf9df857fb5f6a45"},
{file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, {file = "protobuf-3.18.1-cp38-cp38-win32.whl", hash = "sha256:09d9268f6f9da81b7657adcf2fb397524c82f20cdf9e0db3ff4e7567977abd67"},
{file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, {file = "protobuf-3.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6d927774c0ec746fed15a4faff5f44aad0b7a3421fadb6f3ae5ca1f2f8ae26e"},
{file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, {file = "protobuf-3.18.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d19c9cb805fd2be1d59eee39e152367ee92a30167e77bd06c8819f8f0009a4c"},
{file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, {file = "protobuf-3.18.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:387f621bf7295a331f8c8a6962d097ceddeb85356792888cfa6a5c6bfc6886a4"},
{file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, {file = "protobuf-3.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1c5d3966c856f60a9d8d62f4455d70c31026422acdd5c228edf22b65b16c38"},
{file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, {file = "protobuf-3.18.1-cp39-cp39-win32.whl", hash = "sha256:f20f803892f2135e8b96dc58c9a0c6a7ad8436794bf8784af229498d939b4c77"},
{file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, {file = "protobuf-3.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:d76201380f41a2d83fb613a4683059d1fcafbe969518b3e409e279a8788fde2f"},
{file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, {file = "protobuf-3.18.1-py2.py3-none-any.whl", hash = "sha256:61ca58e14033ca0dfa484a31d57237c1be3b6013454c7f53876a20fc88dd69b1"},
{file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, {file = "protobuf-3.18.1.tar.gz", hash = "sha256:1c9bb40503751087300dd12ce2e90899d68628977905c76effc48e66d089391e"},
{file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"},
{file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"},
{file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"},
{file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"},
{file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"},
] ]
py = [ py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
] ]
pyasn1 = [ pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
@@ -719,71 +643,91 @@ pyasn1-modules = [
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
] ]
pycodestyle = [ pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, {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 = [ pyflakes = [
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
] ]
pyparsing = [ pyparsing = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
] ]
pytest = [ pytest = [
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
] ]
python-dateutil = [ python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
] ]
pytz = [ pytz = [
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
] ]
pyyaml = [ pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
] ]
requests = [ requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
] ]
rsa = [ rsa = [
{file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
{file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
] ]
six = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -792,49 +736,24 @@ six = [
termcolor = [ termcolor = [
{file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
] ]
tomli = [ toml = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typed-ast = [
{file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"},
{file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"},
{file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"},
{file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"},
{file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"},
{file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"},
{file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"},
{file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"},
{file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"},
{file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"},
{file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"},
{file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"},
{file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"},
{file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"},
{file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
{file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
{file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
{file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
{file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
{file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
{file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
{file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
{file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
{file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
] ]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
] ]
uritemplate = [ uritemplate = [
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"},
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
] ]
urllib3 = [ urllib3 = [
{file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
{file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
] ]
zipp = [ zipp = [
{file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
] ]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "sync_ics2gcal" name = "sync_ics2gcal"
version = "0.1.4" version = "0.1.3"
description = "Sync ics file with Google calendar" description = "Sync ics file with Google calendar"
authors = ["Dmitry Belyaev <b4tm4n@mail.ru>"] authors = ["Dmitry Belyaev <b4tm4n@mail.ru>"]
license = "MIT" license = "MIT"
@@ -11,6 +11,7 @@ keywords = ["icalendar", "sync", "google", "calendar"]
classifiers = [ classifiers = [
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
@@ -18,18 +19,19 @@ classifiers = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = ">=3.6.1"
google-auth = "2.6.6" google-auth = "2.2.1"
google-api-python-client = "2.49.0" google-api-python-client = "2.23.0"
icalendar = "4.0.9" icalendar = "4.0.7"
pytz = "2022.1" pytz = "2021.1"
PyYAML = "6.0" PyYAML = "5.4.1"
fire = "0.4.0" fire = "0.4.0"
pydantic = "^1.8.2"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.1.2" pytest = "^6.2.5"
flake8 = "^4.0.1" flake8 = "^3.9.2"
black = "^22.3.0" autopep8 = "^1.5.7"
[tool.poetry.scripts] [tool.poetry.scripts]
sync-ics2gcal = "sync_ics2gcal.sync_calendar:main" sync-ics2gcal = "sync_ics2gcal.sync_calendar:main"

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
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

View File

@@ -1,11 +1,18 @@
from .ical import CalendarConverter, EventConverter, DateDateTime
from .ical import (
CalendarConverter,
EventConverter,
DateDateTime
)
from .gcal import ( from .gcal import (
GoogleCalendarService, GoogleCalendarService,
GoogleCalendar, GoogleCalendar,
EventData, EventData,
EventList, EventList,
EventTuple, EventTuple
) )
from .sync import CalendarSync from .sync import (
CalendarSync
)

View File

@@ -7,7 +7,7 @@ from google.oauth2 import service_account
from googleapiclient import discovery from googleapiclient import discovery
from pytz import utc from pytz import utc
EventData = Dict[str, Union[str, "EventData", None]] EventData = Dict[str, Union[str, 'EventData', None]]
EventList = List[EventData] EventList = List[EventData]
EventTuple = Tuple[EventData, EventData] EventTuple = Tuple[EventData, EventData]
@@ -26,25 +26,24 @@ class GoogleCalendarService:
( 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 )
""" """
scopes = ["https://www.googleapis.com/auth/calendar"] scopes = ['https://www.googleapis.com/auth/calendar']
credentials, _ = google.auth.default(scopes=scopes) credentials, _ = google.auth.default(scopes=scopes)
service = discovery.build( service = discovery.build(
"calendar", "v3", credentials=credentials, cache_discovery=False 'calendar', 'v3', credentials=credentials, cache_discovery=False)
)
return service return service
@staticmethod @staticmethod
def from_srv_acc_file(service_account_file: str): 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)
"""
scopes = ["https://www.googleapis.com/auth/calendar"] scopes = ['https://www.googleapis.com/auth/calendar']
credentials = service_account.Credentials.from_service_account_file( credentials = service_account.Credentials.from_service_account_file(
service_account_file service_account_file)
)
scoped_credentials = credentials.with_scopes(scopes) scoped_credentials = credentials.with_scopes(scopes)
service = discovery.build( service = discovery.build(
"calendar", "v3", credentials=scoped_credentials, cache_discovery=False 'calendar', 'v3', credentials=scoped_credentials,
) cache_discovery=False)
return service return service
@staticmethod @staticmethod
@@ -59,8 +58,9 @@ class GoogleCalendarService:
-- None: default credentials will be used -- None: default credentials will be used
""" """
if config is not None and "service_account" in config: if config is not None and 'service_account' in config:
service = GoogleCalendarService.from_srv_acc_file(config["service_account"]) service = GoogleCalendarService.from_srv_acc_file(
config['service_account'])
else: else:
service = GoogleCalendarService.default() service = GoogleCalendarService.default()
return service return service
@@ -77,21 +77,22 @@ def select_event_key(event: EventData) -> Optional[str]:
""" """
key = None key = None
if "iCalUID" in event: if 'iCalUID' in event:
key = "iCalUID" key = 'iCalUID'
elif "id" in event: elif 'id' in event:
key = "id" key = 'id'
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: discovery.Resource, calendar_id: Optional[str]): def __init__(self, service: discovery.Resource, calendarId: Optional[str]):
self.service: discovery.Resource = service self.service: discovery.Resource = service
self.calendar_id: str = calendar_id self.calendarId: str = calendarId
def _make_request_callback(self, action: str, events_by_req: EventList) -> Callable: def _make_request_callback(self, action: str, events_by_req: EventList) -> Callable:
"""make callback for log result of batch request """make callback for log result of batch request
@@ -110,47 +111,39 @@ class GoogleCalendar:
if exception is not None: if exception is not None:
self.logger.error( self.logger.error(
"failed to %s event with %s: %s, exception: %s", 'failed to %s event with %s: %s, exception: %s',
action, action, key, event.get(key), str(exception)
key,
event.get(key),
str(exception),
) )
else: else:
resp_key = select_event_key(response) resp_key = select_event_key(response)
if resp_key is not None: if resp_key is not None:
event = response event = response
key = resp_key key = resp_key
self.logger.info("event %s ok, %s: %s", action, key, event.get(key)) self.logger.info('event %s ok, %s: %s',
action, key, event.get(key))
return callback return callback
def list_events_from(self, start: datetime) -> EventList: def list_events_from(self, start: datetime) -> EventList:
"""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)'
events = [] events = []
page_token = None page_token = None
time_min = ( timeMin = utc.normalize(start.astimezone(utc)).replace(
utc.normalize(start.astimezone(utc)).replace(tzinfo=None).isoformat() + "Z" tzinfo=None).isoformat() + 'Z'
)
while True: while True:
response = ( response = self.service.events().list(calendarId=self.calendarId,
self.service.events()
.list(
calendarId=self.calendar_id,
pageToken=page_token, pageToken=page_token,
singleEvents=True, singleEvents=True,
timeMin=time_min, timeMin=timeMin,
fields=fields, fields=fields).execute()
) if 'items' in response:
.execute() events.extend(response['items'])
) page_token = response.get('nextPageToken')
if "items" in response:
events.extend(response["items"])
page_token = response.get("nextPageToken")
if not page_token: if not page_token:
break break
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: List) -> Tuple[List[EventTuple], EventList]: def find_exists(self, events: List) -> Tuple[List[EventTuple], EventList]:
@@ -164,7 +157,7 @@ class GoogleCalendar:
events_exist - list of tuples: (new_event, exists_event) events_exist - list of tuples: (new_event, exists_event)
""" """
fields = "items(id,iCalUID,updated)" fields = 'items(id,iCalUID,updated)'
events_by_req = [] events_by_req = []
exists = [] exists = []
not_found = [] not_found = []
@@ -173,15 +166,14 @@ class GoogleCalendar:
found = False found = False
cur_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), str(exception), cur_event['iCalUID'])
cur_event["iCalUID"],
)
if found: if found:
exists.append((cur_event, response["items"][0])) exists.append(
(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)])
@@ -189,18 +181,17 @@ class GoogleCalendar:
i = 0 i = 0
for event in events: for event in events:
events_by_req.append(event) events_by_req.append(event)
batch.add( batch.add(self.service.events().list(calendarId=self.calendarId,
self.service.events().list( iCalUID=event['iCalUID'],
calendarId=self.calendar_id,
iCalUID=event["iCalUID"],
showDeleted=True, showDeleted=True,
fields=fields, fields=fields
), ),
request_id=str(i), request_id=str(i)
) )
i += 1 i += 1
batch.execute() batch.execute()
self.logger.info("%d events exists, %d not found", len(exists), len(not_found)) self.logger.info('%d events exists, %d not found',
len(exists), len(not_found))
return exists, not_found return exists, not_found
def insert_events(self, events: EventList): def insert_events(self, events: EventList):
@@ -210,19 +201,17 @@ class GoogleCalendar:
events - events list events - events list
""" """
fields = "id" fields = 'id'
events_by_req = [] events_by_req = []
insert_callback = self._make_request_callback("insert", events_by_req) insert_callback = self._make_request_callback('insert', events_by_req)
batch = self.service.new_batch_http_request(callback=insert_callback) batch = self.service.new_batch_http_request(callback=insert_callback)
i = 0 i = 0
for event in events: for event in events:
events_by_req.append(event) events_by_req.append(event)
batch.add( batch.add(self.service.events().insert(
self.service.events().insert( calendarId=self.calendarId, body=event, fields=fields),
calendarId=self.calendar_id, body=event, fields=fields request_id=str(i)
),
request_id=str(i),
) )
i += 1 i += 1
batch.execute() batch.execute()
@@ -234,23 +223,19 @@ class GoogleCalendar:
event_tuples -- list of tuples: (new_event, exists_event) event_tuples -- list of tuples: (new_event, exists_event)
""" """
fields = "id" fields = 'id'
events_by_req = [] events_by_req = []
patch_callback = self._make_request_callback("patch", events_by_req) patch_callback = self._make_request_callback('patch', events_by_req)
batch = self.service.new_batch_http_request(callback=patch_callback) batch = self.service.new_batch_http_request(callback=patch_callback)
i = 0 i = 0
for event_new, event_old in event_tuples: for event_new, event_old in event_tuples:
if "id" not in event_old: if 'id' not in event_old:
continue continue
events_by_req.append(event_new) events_by_req.append(event_new)
batch.add( batch.add(self.service.events().patch(
self.service.events().patch( calendarId=self.calendarId, eventId=event_old['id'],
calendarId=self.calendar_id, eventId=event_old["id"], body=event_new body=event_new), fields=fields, request_id=str(i))
),
fields=fields,
request_id=str(i),
)
i += 1 i += 1
batch.execute() batch.execute()
@@ -261,25 +246,19 @@ class GoogleCalendar:
event_tuples -- list of tuples: (new_event, exists_event) event_tuples -- list of tuples: (new_event, exists_event)
""" """
fields = "id" fields = 'id'
events_by_req = [] events_by_req = []
update_callback = self._make_request_callback("update", events_by_req) update_callback = self._make_request_callback('update', events_by_req)
batch = self.service.new_batch_http_request(callback=update_callback) batch = self.service.new_batch_http_request(callback=update_callback)
i = 0 i = 0
for event_new, event_old in event_tuples: for event_new, event_old in event_tuples:
if "id" not in event_old: if 'id' not in event_old:
continue continue
events_by_req.append(event_new) events_by_req.append(event_new)
batch.add( batch.add(self.service.events().update(
self.service.events().update( calendarId=self.calendarId, eventId=event_old['id'],
calendarId=self.calendar_id, body=event_new, fields=fields), request_id=str(i))
eventId=event_old["id"],
body=event_new,
fields=fields,
),
request_id=str(i),
)
i += 1 i += 1
batch.execute() batch.execute()
@@ -292,21 +271,18 @@ class GoogleCalendar:
events_by_req = [] events_by_req = []
delete_callback = self._make_request_callback("delete", events_by_req) delete_callback = self._make_request_callback('delete', events_by_req)
batch = self.service.new_batch_http_request(callback=delete_callback) batch = self.service.new_batch_http_request(callback=delete_callback)
i = 0 i = 0
for event in events: for event in events:
events_by_req.append(event) events_by_req.append(event)
batch.add( batch.add(self.service.events().delete(
self.service.events().delete( calendarId=self.calendarId,
calendarId=self.calendar_id, eventId=event["id"] eventId=event['id']), request_id=str(i))
),
request_id=str(i),
)
i += 1 i += 1
batch.execute() batch.execute()
def create(self, summary: str, time_zone: Optional[str] = None) -> Any: def create(self, summary: str, timeZone: Optional[str] = None) -> Any:
"""create calendar """create calendar
Arguments: Arguments:
@@ -319,33 +295,36 @@ class GoogleCalendar:
calendar Resource calendar Resource
""" """
calendar = {"summary": summary} calendar = {'summary': summary}
if time_zone is not None: if timeZone is not None:
calendar["timeZone"] = time_zone calendar['timeZone'] = timeZone
created_calendar = self.service.calendars().insert(body=calendar).execute() created_calendar = self.service.calendars().insert(
self.calendar_id = created_calendar["id"] body=calendar
).execute()
self.calendarId = created_calendar['id']
return created_calendar return created_calendar
def delete(self): def delete(self):
"""delete calendar""" """delete calendar
"""
self.service.calendars().delete(calendarId=self.calendar_id).execute() self.service.calendars().delete(calendarId=self.calendarId).execute()
def make_public(self): def make_public(self):
"""make calendar public""" """make calendar puplic
"""
rule_public = { rule_public = {
"scope": { 'scope': {
"type": "default", 'type': 'default',
}, },
"role": "reader", 'role': 'reader'
} }
return ( return self.service.acl().insert(
self.service.acl() calendarId=self.calendarId,
.insert(calendarId=self.calendar_id, body=rule_public) body=rule_public
.execute() ).execute()
)
def add_owner(self, email: str): def add_owner(self, email: str):
"""add calendar owner by email """add calendar owner by email
@@ -355,14 +334,13 @@ class GoogleCalendar:
""" """
rule_owner = { rule_owner = {
"scope": { 'scope': {
"type": "user", 'type': 'user',
"value": email, 'value': email,
}, },
"role": "owner", 'role': 'owner'
} }
return ( return self.service.acl().insert(
self.service.acl() calendarId=self.calendarId,
.insert(calendarId=self.calendar_id, body=rule_owner) body=rule_owner
.execute() ).execute()
)

View File

@@ -5,11 +5,56 @@ from typing import Union, Dict, Callable, Optional
from icalendar import Calendar, Event from icalendar import Calendar, Event
from pytz import utc from pytz import utc
import pydantic
from .gcal import EventData, EventList from .gcal import EventData, EventList
DateDateTime = Union[datetime.date, datetime.datetime] 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: def format_datetime_utc(value: DateDateTime) -> str:
"""utc datetime as string from date or datetime value """utc datetime as string from date or datetime value
@@ -19,17 +64,20 @@ def format_datetime_utc(value: DateDateTime) -> str:
Returns: Returns:
utc datetime value as string in iso format 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 = datetime.datetime(
value.year, value.month, value.day, tzinfo=utc)
value = value.replace(microsecond=1) value = value.replace(microsecond=1)
return utc.normalize(value.astimezone(utc)).replace(tzinfo=None).isoformat() + "Z" return utc.normalize(
value.astimezone(utc)
).replace(tzinfo=None).isoformat() + 'Z'
def gcal_date_or_datetime( def gcal_date_or_dateTime(value: DateDateTime,
value: DateDateTime, check_value: Optional[DateDateTime] = None check_value: Optional[DateDateTime] = None) \
) -> Dict[str, str]: -> 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: date or datetime
@@ -44,12 +92,12 @@ def gcal_date_or_datetime(
result: Dict[str, str] = {} 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:
if isinstance(check_value, datetime.date): if isinstance(check_value, datetime.date):
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
value = datetime.date(value.year, value.month, value.day) value = datetime.date(value.year, value.month, value.day)
result["date"] = value.isoformat() result['date'] = value.isoformat()
return result return result
@@ -62,13 +110,13 @@ class EventConverter(Event):
"""decoded string property """decoded string property
Arguments: Arguments:
prop - property name prop - propperty name
Returns: Returns:
string value string value
""" """
return self.decoded(prop).decode(encoding="utf-8") return self.decoded(prop).decode(encoding='utf-8')
def _datetime_str_prop(self, prop: str) -> str: def _datetime_str_prop(self, prop: str) -> str:
"""utc datetime as string from property """utc datetime as string from property
@@ -82,7 +130,7 @@ class EventConverter(Event):
return format_datetime_utc(self.decoded(prop)) 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 """ event start dict from icalendar event
Raises: Raises:
@@ -92,10 +140,10 @@ class EventConverter(Event):
dict dict
""" """
value = self.decoded("DTSTART") 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 """event end dict from icalendar event
Raises: Raises:
@@ -104,31 +152,31 @@ class EventConverter(Event):
dict dict
""" """
result: Dict[str, str] result = None
if "DTEND" in self: if 'DTEND' in self:
value = self.decoded("DTEND") value = self.decoded('DTEND')
result = gcal_date_or_datetime(value) result = GCal_DateDateTime.create_from(value)
elif "DURATION" in self: elif 'DURATION' in self:
start_val = self.decoded("DTSTART") start_val = self.decoded('DTSTART')
duration = self.decoded("DURATION") duration = self.decoded('DURATION')
end_val = start_val + 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: else:
raise ValueError("no DTEND or DURATION") raise ValueError('no DTEND or DURATION')
return result return result
def _put_to_gcal( def _put_to_gcal(self, gcal_event: EventData,
self, prop: str, func: Callable[[str], str],
gcal_event: EventData, ics_prop: Optional[str] = None):
prop: str, """get property from ical event if exist, and put to gcal event
func: Callable[[str], str],
ics_prop: Optional[str] = None,
):
"""get property from ical event if existed, and put to gcal event
Arguments: Arguments:
gcal_event -- destination event gcal_event -- dest event
prop -- property name prop -- property name
func -- function to convert func -- function to convert
ics_prop -- ical property name (default: {None}) ics_prop -- ical property name (default: {None})
@@ -139,6 +187,18 @@ 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 _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: def to_gcal(self) -> EventData:
"""Convert """Convert
@@ -146,48 +206,50 @@ class EventConverter(Event):
dict - google calendar#event resource dict - google calendar#event resource
""" """
event = { kwargs = {
"iCalUID": self._str_prop("UID"), 'ical_uid': self._str_prop('UID'),
"start": self._gcal_start(), '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) return GCal_Event(**kwargs).dict(by_alias=True, exclude_defaults=True)
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
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: Optional[Calendar] = None): def __init__(self, calendar: Optional[Calendar] = None):
self.calendar: Optional[Calendar] = calendar self.calendar: Optional[Calendar] = calendar
def load(self, filename: str): 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: str): 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) -> EventList: def events_to_gcal(self) -> EventList:
"""Convert events to google calendar resources""" """Convert events to google calendar resources
"""
ics_events = self.calendar.walk(name="VEVENT") ics_events = self.calendar.walk(name='VEVENT')
self.logger.info("%d events read", len(ics_events)) self.logger.info('%d events readed', len(ics_events))
result = list(map(lambda event: EventConverter(event).to_gcal(), ics_events)) result = list(
self.logger.info("%d events converted", len(result)) map(lambda event: EventConverter(event).to_gcal(), ics_events))
self.logger.info('%d events converted', len(result))
return result return result

View File

@@ -10,7 +10,7 @@ from . import GoogleCalendar, GoogleCalendarService
def load_config(filename: str) -> Optional[Dict[str, Any]]: def load_config(filename: str) -> Optional[Dict[str, Any]]:
result = None result = None
try: try:
with open(filename, "r", encoding="utf-8") as f: with open(filename, 'r', encoding='utf-8') as f:
result = yaml.safe_load(f) result = yaml.safe_load(f)
except FileNotFoundError: except FileNotFoundError:
pass pass
@@ -31,11 +31,8 @@ class PropertyCommands:
calendar_id: calendar id calendar_id: calendar id
property_name: property key property_name: property key
""" """
response = ( response = self._service.calendarList().get(calendarId=calendar_id,
self._service.calendarList() fields=property_name).execute()
.get(calendarId=calendar_id, fields=property_name)
.execute()
)
print(response.get(property_name)) print(response.get(property_name))
def set(self, calendar_id: str, property_name: str, property_value: str) -> None: def set(self, calendar_id: str, property_name: str, property_value: str) -> None:
@@ -47,26 +44,22 @@ class PropertyCommands:
property_value: property value property_value: property value
""" """
body = {property_name: property_value} body = {property_name: property_value}
response = ( response = self._service.calendarList().patch(body=body, calendarId=calendar_id).execute()
self._service.calendarList()
.patch(body=body, calendarId=calendar_id)
.execute()
)
print(response) print(response)
class Commands: class Commands:
""" manage google calendars in service account """ """ manage google calendars in service account """
def __init__(self, config: str = "config.yml"): def __init__(self, config: str = 'config.yml'):
""" """
Args: Args:
config(str): config filename config(str): config filename
""" """
self._config: Optional[Dict[str, Any]] = load_config(config) self._config: Optional[Dict[str, Any]] = load_config(config)
if self._config is not None and "logging" in self._config: if self._config is not None and 'logging' in self._config:
logging.config.dictConfig(self._config["logging"]) logging.config.dictConfig(self._config['logging'])
self._service = GoogleCalendarService.from_config(self._config) self._service = GoogleCalendarService.from_config(self._config)
self.property = PropertyCommands(self._service) self.property = PropertyCommands(self._service)
@@ -78,28 +71,25 @@ class Commands:
show_deleted: show deleted calendars show_deleted: show deleted calendars
""" """
fields: str = "nextPageToken,items(id,summary)" fields: str = 'nextPageToken,items(id,summary)'
calendars: List[Dict[str, Any]] = [] calendars: List[Dict[str, Any]] = []
page_token: Optional[str] = None page_token: Optional[str] = None
while True: while True:
calendars_api = self._service.calendarList() calendars_api = self._service.calendarList()
response = calendars_api.list( response = calendars_api.list(fields=fields,
fields=fields,
pageToken=page_token, pageToken=page_token,
showHidden=show_hidden, showHidden=show_hidden,
showDeleted=show_deleted, showDeleted=show_deleted
).execute() ).execute()
if "items" in response: if 'items' in response:
calendars.extend(response["items"]) calendars.extend(response['items'])
page_token = response.get("nextPageToken") page_token = response.get('nextPageToken')
if page_token is None: if page_token is None:
break break
for calendar in calendars: for calendar in calendars:
print("{summary}: {id}".format_map(calendar)) print('{summary}: {id}'.format_map(calendar))
def create( def create(self, summary: str, timezone: Optional[str] = None, public: bool = False) -> None:
self, summary: str, timezone: Optional[str] = None, public: bool = False
) -> None:
""" create calendar """ create calendar
Args: Args:
@@ -111,7 +101,7 @@ class Commands:
calendar.create(summary, timezone) calendar.create(summary, timezone)
if public: if public:
calendar.make_public() calendar.make_public()
print("{}: {}".format(summary, calendar.calendar_id)) print('{}: {}'.format(summary, calendar.calendarId))
def add_owner(self, calendar_id: str, email: str) -> None: def add_owner(self, calendar_id: str, email: str) -> None:
""" add owner to calendar """ add owner to calendar
@@ -122,7 +112,7 @@ class Commands:
""" """
calendar = GoogleCalendar(self._service, calendar_id) calendar = GoogleCalendar(self._service, calendar_id)
calendar.add_owner(email) calendar.add_owner(email)
print("to {} added owner: {}".format(calendar_id, email)) print('to {} added owner: {}'.format(calendar_id, email))
def remove(self, calendar_id: str) -> None: def remove(self, calendar_id: str) -> None:
""" remove calendar """ remove calendar
@@ -132,7 +122,7 @@ class Commands:
""" """
calendar = GoogleCalendar(self._service, calendar_id) calendar = GoogleCalendar(self._service, calendar_id)
calendar.delete() calendar.delete()
print("removed: {}".format(calendar_id)) print('removed: {}'.format(calendar_id))
def rename(self, calendar_id: str, summary: str) -> None: def rename(self, calendar_id: str, summary: str) -> None:
""" rename calendar """ rename calendar
@@ -141,14 +131,14 @@ class Commands:
calendar_id: calendar id calendar_id: calendar id
summary: summary:
""" """
calendar = {"summary": summary} calendar = {'summary': summary}
self._service.calendars().patch(body=calendar, calendarId=calendar_id).execute() self._service.calendars().patch(body=calendar, calendarId=calendar_id).execute()
print("{}: {}".format(summary, calendar_id)) print('{}: {}'.format(summary, calendar_id))
def main(): def main():
fire.Fire(Commands, name="manage-ics2gcal") fire.Fire(Commands, name='manage-ics2gcal')
if __name__ == "__main__": if __name__ == '__main__':
main() main()

View File

@@ -11,9 +11,10 @@ from .ical import CalendarConverter, DateDateTime
class CalendarSync: class CalendarSync:
"""class for synchronize calendar with Google""" """class for syncronize calendar with google
"""
logger = logging.getLogger("CalendarSync") logger = logging.getLogger('CalendarSync')
def __init__(self, gcalendar: GoogleCalendar, converter: CalendarConverter): def __init__(self, gcalendar: GoogleCalendar, converter: CalendarConverter):
self.gcalendar: GoogleCalendar = gcalendar self.gcalendar: GoogleCalendar = gcalendar
@@ -23,14 +24,15 @@ class CalendarSync:
self.to_delete: EventList = [] self.to_delete: EventList = []
@staticmethod @staticmethod
def _events_list_compare( def _events_list_compare(items_src: EventList,
items_src: EventList, items_dst: EventList, key: str = "iCalUID" items_dst: EventList,
) -> Tuple[EventList, List[EventTuple], EventList]: key: str = 'iCalUID') \
-> Tuple[EventList, List[EventTuple], EventList]:
""" compare list of events by key """ compare list of events by key
Arguments: Arguments:
items_src {list of dict} -- source events items_src {list of dict} -- source events
items_dst {list of dict} -- destination events items_dst {list of dict} -- dest events
key {str} -- name of key to compare (default: {'iCalUID'}) key {str} -- name of key to compare (default: {'iCalUID'})
Returns: Returns:
@@ -39,8 +41,7 @@ class CalendarSync:
items_to_delete) items_to_delete)
""" """
def get_key(item: EventData) -> str: def get_key(item: EventData) -> str: return item[key]
return item[key]
keys_src: Set[str] = set(map(get_key, items_src)) keys_src: Set[str] = set(map(get_key, items_src))
keys_dst: Set[str] = set(map(get_key, items_dst)) keys_dst: Set[str] = set(map(get_key, items_dst))
@@ -49,7 +50,9 @@ class CalendarSync:
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: EventList, key_name: str, keys: Set[str]) -> EventList: def items_by_keys(items: EventList,
key_name: str,
keys: Set[str]) -> EventList:
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)
@@ -64,53 +67,50 @@ class CalendarSync:
return items_to_insert, items_to_update, items_to_delete return items_to_insert, items_to_update, items_to_delete
def _filter_events_to_update(self): def _filter_events_to_update(self):
"""filter 'to_update' events by 'updated' datetime""" """ filter 'to_update' events by 'updated' datetime
"""
def filter_updated(event_tuple: EventTuple) -> bool: def filter_updated(event_tuple: EventTuple) -> bool:
new, old = event_tuple new, old = event_tuple
if "updated" not in new or "updated" not in old: new_date = dateutil.parser.parse(new['updated'])
return True old_date = dateutil.parser.parse(old['updated'])
new_date = dateutil.parser.parse(new["updated"])
old_date = dateutil.parser.parse(old["updated"])
return new_date > old_date return new_date > old_date
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( def _filter_events_by_date(events: EventList,
events: EventList,
date: DateDateTime, date: DateDateTime,
op: Callable[[DateDateTime, DateDateTime], bool], op: Callable[[DateDateTime,
) -> EventList: DateDateTime], bool]) -> EventList:
""" filter events by start datetime """ filter events by start datetime
Arguments: Arguments:
events -- events list events -- events list
date {datetime} -- datetime to compare date {datetime} -- datetime to compare
op {operator} -- comparison operator op {operator} -- comparsion operator
Returns: Returns:
list of filtered events list of filtred events
""" """
def filter_by_date(event: EventData) -> bool: def filter_by_date(event: EventData) -> bool:
date_cmp = date date_cmp = date
event_start: Dict[str, str] = event["start"] event_start: Dict[str, str] = event['start']
event_date: Union[DateDateTime, str, None] = None event_date: Union[DateDateTime, str, None] = None
compare_dates = False compare_dates = False
if "date" in event_start: if 'date' in event_start:
event_date = event_start["date"] event_date = event_start['date']
compare_dates = True compare_dates = True
elif "dateTime" in event_start: elif 'dateTime' in event_start:
event_date = event_start["dateTime"] event_date = event_start['dateTime']
event_date = dateutil.parser.parse(event_date) event_date = dateutil.parser.parse(event_date)
if compare_dates: if compare_dates:
date_cmp = datetime.date(date.year, date.month, date.day) date_cmp = datetime.date(date.year, date.month, date.day)
event_date = datetime.date( event_date = datetime.date(
event_date.year, event_date.month, event_date.day event_date.year, event_date.month, event_date.day)
)
return op(event_date, date_cmp) return op(event_date, date_cmp)
@@ -118,13 +118,13 @@ class CalendarSync:
@staticmethod @staticmethod
def _tz_aware_datetime(date: DateDateTime) -> datetime.datetime: def _tz_aware_datetime(date: DateDateTime) -> datetime.datetime:
"""make tz aware datetime from datetime/date (utc if no tz-info) """make tz aware datetime from datetime/date (utc if no tzinfo)
Arguments: Arguments:
date - date or datetime / with or without tz-info date - date or datetime / with or without tzinfo
Returns: Returns:
datetime with tz-info datetime with tzinfo
""" """
if not isinstance(date, datetime.datetime): if not isinstance(date, datetime.datetime):
@@ -134,7 +134,7 @@ class CalendarSync:
return date return date
def prepare_sync(self, start_date: DateDateTime) -> None: def prepare_sync(self, start_date: DateDateTime) -> None:
"""prepare sync lists by comparison of events """prepare sync lists by comparsion of events
Arguments: Arguments:
start_date -- date/datetime to start sync start_date -- date/datetime to start sync
@@ -147,47 +147,44 @@ class CalendarSync:
# divide source events by start datetime # divide source events by start datetime
events_src_pending = CalendarSync._filter_events_by_date( events_src_pending = CalendarSync._filter_events_by_date(
events_src, start_date, operator.ge events_src, start_date, operator.ge)
)
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)
)
# first events comparison # first events comparsion
( self.to_insert, self.to_update, self.to_delete = CalendarSync._events_list_compare(
self.to_insert, events_src_pending, events_dst)
self.to_update,
self.to_delete,
) = CalendarSync._events_list_compare(events_src_pending, events_dst)
# 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)
# 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(self.to_insert) add_to_update, self.to_insert = self.gcalendar.find_exists(
self.to_insert)
self.to_update.extend(add_to_update) self.to_update.extend(add_to_update)
# 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()
self.logger.info( self.logger.info(
"prepared to sync: ( insert: %d, update: %d, delete: %d )", 'prepared to sync: ( insert: %d, update: %d, delete: %d )',
len(self.to_insert), len(self.to_insert),
len(self.to_update), len(self.to_update),
len(self.to_delete), len(self.to_delete)
) )
def clear(self) -> None: def clear(self) -> None:
"""clear prepared sync lists (insert, update, delete)""" """ clear prepared sync lists (insert, update, delete)
"""
self.to_insert.clear() self.to_insert.clear()
self.to_update.clear() self.to_update.clear()
self.to_delete.clear() self.to_delete.clear()
def apply(self) -> None: def apply(self) -> None:
"""apply sync (insert, update, delete), using prepared lists of events""" """ 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)
@@ -195,4 +192,4 @@ class CalendarSync:
self.clear() self.clear()
self.logger.info("sync done") self.logger.info('sync done')

View File

@@ -1,4 +1,4 @@
from typing import Dict, Any, Union from typing import Dict, Any
import yaml import yaml
@@ -6,48 +6,49 @@ import dateutil.parser
import datetime import datetime
import logging import logging
import logging.config import logging.config
from . import CalendarConverter, GoogleCalendarService, GoogleCalendar, CalendarSync from . import (
CalendarConverter,
ConfigDate = Union[str, datetime.datetime] GoogleCalendarService,
GoogleCalendar,
CalendarSync
)
def load_config() -> Dict[str, Any]: 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: ConfigDate) -> datetime.datetime: def get_start_date(date_str: str) -> datetime.datetime:
if isinstance(date, datetime.datetime): if 'now' == date_str:
return date
if "now" == date:
result = datetime.datetime.utcnow() result = datetime.datetime.utcnow()
else: else:
result = dateutil.parser.parse(date) result = dateutil.parser.parse(date_str)
return result return result
def main(): def main():
config = load_config() config = load_config()
if "logging" in config: if 'logging' in config:
logging.config.dictConfig(config["logging"]) logging.config.dictConfig(config['logging'])
calendar_id: str = config["calendar"]["google_id"] calendarId: str = config['calendar']['google_id']
ics_filepath: str = config["calendar"]["source"] ics_filepath: str = config['calendar']['source']
start = get_start_date(config["start_from"]) start = get_start_date(config['start_from'])
converter = CalendarConverter() converter = CalendarConverter()
converter.load(ics_filepath) converter.load(ics_filepath)
service = GoogleCalendarService.from_config(config) service = GoogleCalendarService.from_config(config)
gcalendar = GoogleCalendar(service, calendar_id) gcalendar = GoogleCalendar(service, calendarId)
sync = CalendarSync(gcalendar, converter) sync = CalendarSync(gcalendar, converter)
sync.prepare_sync(start) sync.prepare_sync(start)
sync.apply() sync.apply()
if __name__ == "__main__": if __name__ == '__main__':
main() main()

105
tests/bench_converter.py Normal file
View File

@@ -0,0 +1,105 @@
from typing import Iterable, List, Tuple, Union, Optional
from uuid import uuid4
import datetime
from itertools import islice
from dataclasses import dataclass
import time
import statistics
import functools
from sync_ics2gcal import CalendarConverter
@dataclass
class IcsTestEvent:
uid: str
start_date: Union[datetime.datetime, datetime.date]
end_date: Union[datetime.datetime, datetime.date, None] = None
duration: Optional[datetime.timedelta] = None
created: Union[datetime.datetime, datetime.date, None] = None
updated: Union[datetime.datetime, datetime.date, None] = None
@staticmethod
def _format_datetime(value: Union[datetime.datetime, datetime.date]):
result: str = ''
if isinstance(value, datetime.datetime):
result += f'DATE-TIME:{value.strftime("%Y%m%dT%H%M%SZ")}'
else:
result += f'DATE:{value.strftime("%Y%m%d")}'
return result
def render(self) -> str:
result: str = ''
result += 'BEGIN:VEVENT\r\n'
result += f'UID:{self.uid}\r\n'
result += f'DTSTART;VALUE={IcsTestEvent._format_datetime(self.start_date)}\r\n'
if self.end_date is not None:
result += f'DTEND;VALUE={IcsTestEvent._format_datetime(self.end_date)}\r\n'
else:
result += f'DURATION:P{self.duration.days}D\r\n'
if self.created is not None:
result += f'CREATED:{self.created.strftime("%Y%m%dT%H%M%SZ")}\r\n'
if self.updated is not None:
result += f'LAST-MODIFIED:{self.updated.strftime("%Y%m%dT%H%M%SZ")}\r\n'
result += 'END:VEVENT\r\n'
return result
@dataclass
class IcsTestCalendar:
events: List[IcsTestEvent]
def render(self) -> str:
result: str = ''
result += 'BEGIN:VCALENDAR\r\n'
for event in self.events:
result += event.render()
result += 'END:VCALENDAR\r\n'
return result
def gen_test_calendar(events_count: int) -> IcsTestCalendar:
def gen_events() -> Iterable[IcsTestEvent]:
for i in range(10000000):
uid = f'{uuid4()}@test.com'
start_date = datetime.datetime.now() + datetime.timedelta(hours=i)
end_date = start_date + datetime.timedelta(hours=1)
event: IcsTestEvent = IcsTestEvent(
uid=uid, start_date=start_date, end_date=end_date, created=start_date, updated=start_date)
yield event
events: List[IcsTestEvent] = list(islice(gen_events(), events_count))
result: IcsTestCalendar = IcsTestCalendar(events)
return result
test_calendar: IcsTestCalendar = gen_test_calendar(1000)
ics_test_calendar: str = test_calendar.render()
converter = CalendarConverter()
converter.loads(ics_test_calendar)
def bench(num_iters=1000):
def make_wrapper(func):
@functools.wraps(func)
def wrapper(*args, **kw):
times = []
for _ in range(num_iters):
t0 = time.perf_counter_ns()
result = func(*args, **kw)
t1 = time.perf_counter_ns()
times.append(t1 - t0)
best = min(times)
avg = round(sum(times) / num_iters, 2)
median = statistics.median(times)
print(
f'{func.__name__} x {num_iters} => best: {best} ns, \tavg: {avg} ns, \tmedian: {median} ns')
return result
return wrapper()
return make_wrapper
@bench(num_iters=500)
def events_to_gcal():
converter.events_to_gcal()

View File

@@ -1,52 +1,31 @@
import datetime
from typing import Tuple from typing import Tuple
import pytest import pytest
from pytz import timezone, utc
from sync_ics2gcal import CalendarConverter from sync_ics2gcal import CalendarConverter
from sync_ics2gcal.ical import format_datetime_utc
uid = "UID:uisgtr8tre93wewe0yr8wqy@test.com" uid = "UID:uisgtr8tre93wewe0yr8wqy@test.com"
only_start_date = ( only_start_date = uid + """
uid
+ """
DTSTART;VALUE=DATE:20180215 DTSTART;VALUE=DATE:20180215
""" """
) date_val = only_start_date + """
date_val = (
only_start_date
+ """
DTEND;VALUE=DATE:20180217 DTEND;VALUE=DATE:20180217
""" """
) date_duration = only_start_date + """
date_duration = (
only_start_date
+ """
DURATION:P2D DURATION:P2D
""" """
) datetime_utc_val = uid + """
datetime_utc_val = (
uid
+ """
DTSTART;VALUE=DATE-TIME:20180319T092001Z DTSTART;VALUE=DATE-TIME:20180319T092001Z
DTEND:20180321T102501Z DTEND:20180321T102501Z
""" """
) datetime_utc_duration = uid + """
datetime_utc_duration = (
uid
+ """
DTSTART;VALUE=DATE-TIME:20180319T092001Z DTSTART;VALUE=DATE-TIME:20180319T092001Z
DURATION:P2DT1H5M DURATION:P2DT1H5M
""" """
) created_updated = date_val + """
created_updated = (
date_val
+ """
CREATED:20180320T071155Z CREATED:20180320T071155Z
LAST-MODIFIED:20180326T120235Z LAST-MODIFIED:20180326T120235Z
""" """
)
def ics_test_cal(content: str) -> str: def ics_test_cal(content: str) -> str:
@@ -78,29 +57,14 @@ def test_event_no_end():
converter.events_to_gcal() converter.events_to_gcal()
@pytest.fixture( @pytest.fixture(params=[
params=[ ("date", ics_test_event(date_val), '2018-02-15', '2018-02-17'),
("date", ics_test_event(date_val), "2018-02-15", "2018-02-17"), ("date", ics_test_event(date_duration), '2018-02-15', '2018-02-17'),
("date", ics_test_event(date_duration), "2018-02-15", "2018-02-17"), ("dateTime", ics_test_event(datetime_utc_val),
( '2018-03-19T09:20:01.000001Z', '2018-03-21T10:25:01.000001Z'),
"dateTime", ("dateTime", ics_test_event(datetime_utc_duration), '2018-03-19T09:20:01.000001Z', '2018-03-21T10:25:01.000001Z')],
ics_test_event(datetime_utc_val), ids=['date values', 'date duration',
"2018-03-19T09:20:01.000001Z", 'datetime utc values', 'datetime utc duration']
"2018-03-21T10:25:01.000001Z",
),
(
"dateTime",
ics_test_event(datetime_utc_duration),
"2018-03-19T09:20:01.000001Z",
"2018-03-21T10:25:01.000001Z",
),
],
ids=[
"date values",
"date duration",
"datetime utc values",
"datetime utc duration",
],
) )
def param_events_start_end(request): def param_events_start_end(request):
return request.param return request.param
@@ -113,8 +77,12 @@ def test_event_start_end(param_events_start_end: Tuple[str, str, str, str]):
events = converter.events_to_gcal() events = converter.events_to_gcal()
assert len(events) == 1 assert len(events) == 1
event = events[0] event = events[0]
assert event["start"] == {date_type: start} assert event['start'] == {
assert event["end"] == {date_type: end} date_type: start
}
assert event['end'] == {
date_type: end
}
def test_event_created_updated(): def test_event_created_updated():
@@ -123,24 +91,5 @@ def test_event_created_updated():
events = converter.events_to_gcal() events = converter.events_to_gcal()
assert len(events) == 1 assert len(events) == 1
event = events[0] event = events[0]
assert event["created"] == "2018-03-20T07:11:55.000001Z" assert event['created'] == '2018-03-20T07:11:55.000001Z'
assert event["updated"] == "2018-03-26T12:02:35.000001Z" assert event['updated'] == '2018-03-26T12:02:35.000001Z'
@pytest.mark.parametrize(
"value,expected_str",
[
(
datetime.datetime(2022, 6, 3, 13, 52, 15, 1, utc),
"2022-06-03T13:52:15.000001Z",
),
(
datetime.datetime(2022, 6, 3, 13, 52, 15, 1, timezone("Europe/Moscow")),
"2022-06-03T11:22:15.000001Z",
),
(datetime.date(2022, 6, 3), "2022-06-03T00:00:00.000001Z"),
],
ids=["utc", "with timezone", "date"],
)
def test_format_datetime_utc(value: datetime.datetime, expected_str: str):
assert format_datetime_utc(value) == expected_str

View File

@@ -14,30 +14,28 @@ from sync_ics2gcal import CalendarSync
def sha1(string: Union[str, bytes]) -> str: 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()
h.update(string) h.update(string)
return h.hexdigest() return h.hexdigest()
def gen_events( def gen_events(start: int,
start: int,
stop: int, stop: int,
start_time: Union[datetime.datetime, datetime.date], start_time: Union[datetime.datetime, datetime.date],
no_time: bool = False, no_time: bool = False) -> List[Dict[str, Union[str, Dict[str, str]]]]:
) -> List[Dict[str, Union[str, Dict[str, str]]]]:
if no_time: if no_time:
start_time = datetime.date(start_time.year, start_time.month, start_time.day) start_time = datetime.date(
start_time.year, start_time.month, start_time.day)
duration: datetime.timedelta = 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: str = "date" date_key: str = "date"
date_end: str = "" date_end: str = ''
else: else:
start_time = utc.normalize(start_time.astimezone(utc)).replace(tzinfo=None) start_time = utc.normalize(
duration: datetime.timedelta = datetime.datetime( start_time.astimezone(utc)).replace(tzinfo=None)
1, 1, 1, 2 duration: datetime.timedelta = datetime.datetime(1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1)
) - datetime.datetime(1, 1, 1, 1)
date_key: str = "dateTime" date_key: str = "dateTime"
date_end: str = "Z" date_end: str = 'Z'
result: List[Dict[str, Union[str, Dict[str, str]]]] = [] result: List[Dict[str, Union[str, Dict[str, str]]]] = []
for i in range(start, stop): for i in range(start, stop):
@@ -47,18 +45,17 @@ def gen_events(
updated: Union[datetime.datetime, datetime.date] = 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: Dict[str, Union[str, Dict[str, str]]] = { 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}, 'start': {date_key: event_start.isoformat() + date_end},
"end": {date_key: event_end.isoformat() + date_end}, 'end': {date_key: event_end.isoformat() + date_end}
} }
result.append(event) result.append(event)
return result return result
@@ -67,21 +64,19 @@ def gen_events(
def gen_list_to_compare(start: int, stop: int) -> List[Dict[str, str]]: def gen_list_to_compare(start: int, stop: int) -> List[Dict[str, str]]:
result: List[Dict[str, str]] = [] 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( def get_start_date(event: Dict[str, Union[str, Dict[str, str]]]) -> Union[datetime.datetime, datetime.date]:
event: Dict[str, Union[str, Dict[str, str]]] event_start: Dict[str, str] = event['start']
) -> Union[datetime.datetime, datetime.date]:
event_start: Dict[str, str] = event["start"]
start_date: Optional[str] = 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']
is_date = True is_date = True
if "dateTime" in event_start: if 'dateTime' in event_start:
start_date = event_start["dateTime"] start_date = event_start['dateTime']
result = dateutil.parser.parse(start_date) result = dateutil.parser.parse(start_date)
if is_date: if is_date:
@@ -95,7 +90,8 @@ def test_compare():
# [1..2n] # [1..2n]
lst_src = gen_list_to_compare(1, 1 + part_len * 2) lst_src = gen_list_to_compare(1, 1 + part_len * 2)
# [n..3n] # [n..3n]
lst_dst = gen_list_to_compare(1 + part_len, 1 + part_len * 3) lst_dst = gen_list_to_compare(
1 + part_len, 1 + part_len * 3)
lst_src_rnd = deepcopy(lst_src) lst_src_rnd = deepcopy(lst_src)
lst_dst_rnd = deepcopy(lst_dst) lst_dst_rnd = deepcopy(lst_dst)
@@ -103,14 +99,15 @@ def test_compare():
shuffle(lst_src_rnd) shuffle(lst_src_rnd)
shuffle(lst_dst_rnd) shuffle(lst_dst_rnd)
to_ins, to_upd, to_del = CalendarSync._events_list_compare(lst_src_rnd, lst_dst_rnd) to_ins, to_upd, to_del = CalendarSync._events_list_compare(
lst_src_rnd, lst_dst_rnd)
assert len(to_ins) == part_len assert len(to_ins) == part_len
assert len(to_upd) == part_len assert len(to_upd) == part_len
assert len(to_del) == part_len assert len(to_del) == part_len
assert sorted(to_ins, key=lambda x: x["iCalUID"]) == lst_src[:part_len] assert sorted(to_ins, key=lambda x: x['iCalUID']) == lst_src[:part_len]
assert sorted(to_del, key=lambda x: x["iCalUID"]) == lst_dst[part_len:] assert sorted(to_del, key=lambda x: x['iCalUID']) == lst_dst[part_len:]
to_upd_ok = list(zip(lst_src[part_len:], lst_dst[:part_len])) to_upd_ok = list(zip(lst_src[part_len:], lst_dst[:part_len]))
assert len(to_upd) == len(to_upd_ok) assert len(to_upd) == len(to_upd_ok)
@@ -118,29 +115,35 @@ def test_compare():
assert item in to_upd assert item in to_upd
@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: bool): 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))
part_len = 5 part_len = 5
if no_time: if no_time:
duration = datetime.date(1, 1, 2) - datetime.date(1, 1, 1) duration = datetime.date(
1, 1, 2) - datetime.date(1, 1, 1)
else: else:
duration = datetime.datetime(1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1) duration = datetime.datetime(
1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1)
date_cmp = msk_now + (duration * part_len) date_cmp = msk_now + (duration * part_len)
if no_time: if no_time:
date_cmp = datetime.date(date_cmp.year, date_cmp.month, date_cmp.day) date_cmp = datetime.date(
date_cmp.year, date_cmp.month, date_cmp.day)
events = gen_events(1, 1 + (part_len * 2), msk_now, no_time) events = gen_events(
1, 1 + (part_len * 2), msk_now, no_time)
shuffle(events) shuffle(events)
events_pending = CalendarSync._filter_events_by_date(events, date_cmp, operator.ge) events_pending = CalendarSync._filter_events_by_date(
events_past = CalendarSync._filter_events_by_date(events, date_cmp, operator.lt) events, date_cmp, operator.ge)
events_past = CalendarSync._filter_events_by_date(
events, date_cmp, operator.lt)
assert len(events_pending) == 1 + part_len assert len(events_pending) == 1 + part_len
assert len(events_past) == part_len - 1 assert len(events_past) == part_len - 1
@@ -153,11 +156,12 @@ def test_filter_events_by_date(no_time: bool):
def test_filter_events_to_update(): def test_filter_events_to_update():
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))
one_hour = datetime.datetime(1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1) one_hour = datetime.datetime(
1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1)
date_upd = msk_now + (one_hour * 5) date_upd = msk_now + (one_hour * 5)
count = 10 count = 10
@@ -174,30 +178,3 @@ def test_filter_events_to_update():
assert len(sync1.to_update) == count assert len(sync1.to_update) == count
assert sync2.to_update == [] assert sync2.to_update == []
def test_filter_events_no_updated():
"""
test filtering events that not have 'updated' field
such events should always pass the filter
"""
now = datetime.datetime.utcnow()
yesterday = now - datetime.timedelta(days=-1)
count = 10
events_old = gen_events(1, 1 + count, now)
events_new = gen_events(1, 1 + count, now)
# 1/2 updated=yesterday, 1/2 no updated field
i = 0
for event in events_new:
if 0 == i % 2:
event["updated"] = yesterday.isoformat() + "Z"
else:
del event["updated"]
i += 1
sync = CalendarSync(None, None)
sync.to_update = list(zip(events_old, events_new))
sync._filter_events_to_update()
assert len(sync.to_update) == count // 2