mirror of
https://github.com/b4tman/sync_ics2gcal
synced 2026-02-04 07:14:59 +00:00
Compare commits
89 Commits
v0.1.1
...
converter/
| Author | SHA1 | Date | |
|---|---|---|---|
|
d2d43d02da
|
|||
|
2e114db5c9
|
|||
|
2ab96d1b00
|
|||
|
15951ba200
|
|||
|
d03e5691ee
|
|||
|
3686bc29ee
|
|||
|
9603718c83
|
|||
|
f7a84cc58c
|
|||
|
782ab05126
|
|||
|
d08671e722
|
|||
|
df2f3ef483
|
|||
|
9e40b6fbbd
|
|||
|
b86ef30397
|
|||
|
|
2b146bf15d | ||
|
|
6d4a9ddf30 | ||
|
|
217a1ccf39 | ||
|
|
1bc2fe1ea2 | ||
|
|
1187aa0f3e | ||
|
|
c3312956e2 | ||
|
|
8910768b61 | ||
|
|
3274b85ca6 | ||
|
|
8d4a21c72d | ||
|
|
412c916b15 | ||
|
052ba440d0
|
|||
|
5e01b6dd01
|
|||
|
|
8b2f35b3a9 | ||
|
c3bdd25d5a
|
|||
|
3b0de9d636
|
|||
|
|
97614ae21d | ||
|
|
1cdf1da6ee | ||
|
|
bd6bd65719 | ||
|
c41b3a4dbd
|
|||
|
9dab3c5709
|
|||
|
e5064eeaed
|
|||
|
19192d1641
|
|||
|
6c571df7bc
|
|||
|
a6474ee984
|
|||
|
8669aefabe
|
|||
|
77e2cdba36
|
|||
|
|
18224ad5b4 | ||
|
|
41c2973646 | ||
|
|
a02775110d | ||
|
|
12653df1bf | ||
|
|
fef3586146 | ||
|
b7cd3847bc
|
|||
| 3ddc486614 | |||
|
|
1beff774bc | ||
|
e0b4e6c28a
|
|||
|
|
50f90925b8 | ||
|
|
c2a3a54783 | ||
|
|
38dd853436 | ||
|
|
a10e62f806 | ||
|
|
eca648ee56 | ||
|
|
55ee5002cc | ||
|
|
1bec98a53e | ||
|
|
5649a71da2 | ||
|
|
fc490dcefe | ||
|
|
ecb2f5a3d1 | ||
| 8f56ad426e | |||
|
|
a5739cb64c | ||
|
|
fff533c0a8 | ||
|
|
d8cb345550 | ||
|
|
05a4770071 | ||
|
|
7664ca9e55 | ||
|
|
66f9de3980 | ||
|
|
a7164abb24 | ||
|
|
38f7403b40 | ||
|
|
9ad544971e | ||
|
|
2d00ae77c9 | ||
|
|
283b164723 | ||
|
|
6058a3e592 | ||
|
|
9c08630931 | ||
|
|
9e027df349 | ||
|
|
c1d148c3f3 | ||
|
|
694b91798e | ||
|
8712b81a53
|
|||
|
51005fb29e
|
|||
|
6e7c3cb7b2
|
|||
|
0f9a8d7a74
|
|||
|
0161d65c16
|
|||
|
9e74772852
|
|||
|
8d64869f06
|
|||
|
ab00cb09c8
|
|||
|
b0a39a1b8c
|
|||
|
55b67469be
|
|||
|
c17d3cd0ea
|
|||
|
9aad7e1910
|
|||
|
|
41cc6b4159 | ||
|
5d37aa2a33
|
@@ -1 +0,0 @@
|
||||
ref-names: $Format:%D$
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
.git_archival.txt export-subst
|
||||
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: '02:00'
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: develop
|
||||
33
.github/workflows/codeql-analysis.yml
vendored
Normal file
33
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop, ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [develop]
|
||||
schedule:
|
||||
- cron: '0 12 10 * *'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
32
.github/workflows/pythonpackage.yml
vendored
32
.github/workflows/pythonpackage.yml
vendored
@@ -13,30 +13,34 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: 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
|
||||
uses: snok/install-poetry@v1
|
||||
- name: Install deps
|
||||
run: poetry install
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
pip install flake8
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
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
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
poetry run flake8 sync_ics2gcal --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pip install pytest
|
||||
pytest -v
|
||||
run: poetry run pytest -v
|
||||
|
||||
27
.github/workflows/pythonpublish.yml
vendored
27
.github/workflows/pythonpublish.yml
vendored
@@ -10,17 +10,24 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools setuptools_scm setuptools_scm_git_archive wheel twine
|
||||
- name: Build and publish
|
||||
- name: 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
|
||||
uses: snok/install-poetry@v1
|
||||
- name: Install deps
|
||||
run: poetry install
|
||||
- name: Build
|
||||
run: poetry build
|
||||
- name: Publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.pypi_token }}
|
||||
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.pypi_token }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
poetry publish -n
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,8 +2,10 @@ config.yml
|
||||
service-account.json
|
||||
*.pyc
|
||||
my-test*.ics
|
||||
.vscode/*
|
||||
.vscode/
|
||||
.idea/
|
||||
/dist/
|
||||
/*.egg-info/
|
||||
/build/
|
||||
/.eggs/
|
||||
venv/
|
||||
|
||||
15
.travis.yml
15
.travis.yml
@@ -1,10 +1,21 @@
|
||||
language: python
|
||||
os: linux
|
||||
dist: focal
|
||||
|
||||
python:
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
|
||||
before_install:
|
||||
- pip install poetry
|
||||
install:
|
||||
- poetry install
|
||||
script:
|
||||
- pytest -v
|
||||
# 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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
include pyproject.toml
|
||||
|
||||
# Include the README
|
||||
include *.md
|
||||
|
||||
# Include the license file
|
||||
include LICENSE
|
||||
49
README.md
49
README.md
@@ -2,8 +2,6 @@
|
||||
|
||||
[](https://badge.fury.io/py/sync-ics2gcal)
|
||||
[](https://travis-ci.org/b4tman/sync_ics2gcal)
|
||||
[](https://dependabot.com)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield)
|
||||

|
||||
|
||||
Python scripts for sync .ics file with Google calendar
|
||||
@@ -12,21 +10,26 @@ Python scripts for sync .ics file with Google calendar
|
||||
|
||||
To install from [PyPI](https://pypi.org/project/sync-ics2gcal/) with [pip](https://pypi.python.org/pypi/pip), run:
|
||||
|
||||
```
|
||||
```sh
|
||||
pip install sync-ics2gcal
|
||||
```
|
||||
|
||||
Or download source code and install using poetry:
|
||||
|
||||
Or download source code and install:
|
||||
|
||||
```
|
||||
python setup.py install
|
||||
```sh
|
||||
# install poetry
|
||||
pip install poetry
|
||||
# install project and deps to virtualenv
|
||||
poetry install
|
||||
# run
|
||||
poetry run sync-ics2gcal
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Create application in Google API Console
|
||||
1. Create a new project: https://console.developers.google.com/project
|
||||
|
||||
1. Create a new project: [console.developers.google.com/project](https://console.developers.google.com/project)
|
||||
2. Choose the new project from the top right project dropdown (only if another project is selected)
|
||||
3. In the project Dashboard, choose "Library"
|
||||
4. Find and Enable "Google Calendar API"
|
||||
@@ -38,15 +41,20 @@ python setup.py install
|
||||
10. Edit service account and click "Create key", choose JSON and download key file.
|
||||
|
||||
### Create working directory
|
||||
|
||||
For example: `/home/user/myfolder`.
|
||||
|
||||
1. Save service account key in file `service-account.json`.
|
||||
2. Download [sample config](https://github.com/b4tman/sync_ics2gcal/blob/develop/sample-config.yml) and save to file `config.yml`. For example:
|
||||
```
|
||||
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-config.yml -O config.yml
|
||||
```
|
||||
|
||||
3. *(Optional)* Place source `.ics` file, `my-calendar.ics` for example.
|
||||
|
||||
### Configuration parameters
|
||||
|
||||
* `start_from` - start date:
|
||||
* full format datetime, `2018-04-03T13:23:25.000001Z` for example
|
||||
* or just `now`
|
||||
@@ -55,36 +63,39 @@ wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-confi
|
||||
* `google_id` - target google calendar id, `my-calendar@group.calendar.google.com` for example
|
||||
* `source` - source `.ics` filename, `my-calendar.ics` for example
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Manage calendars
|
||||
|
||||
```
|
||||
manage-ics2gcal <subcommand> [-h] [options]
|
||||
```sh
|
||||
manage-ics2gcal GROUP | COMMAND
|
||||
```
|
||||
|
||||
subcomands:
|
||||
**GROUPS**:
|
||||
|
||||
* **property** - get/set properties (see [CalendarList resource](https://developers.google.com/calendar/v3/reference/calendarList#resource)), subcommands:
|
||||
- **get** - get calendar property
|
||||
- **set** - set calendar property
|
||||
|
||||
**COMMANDS**:
|
||||
|
||||
* **list** - list calendars
|
||||
* **create** - create calendar
|
||||
* **add_owner** - add owner to calendar
|
||||
* **remove** - remove calendar
|
||||
* **rename** - rename calendar
|
||||
|
||||
|
||||
Use **-h** for more info.
|
||||
|
||||
### Sync calendar
|
||||
|
||||
just type:
|
||||
```
|
||||
|
||||
```sh
|
||||
sync-ics2gcal
|
||||
```
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||

|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_large)
|
||||
759
poetry.lock
generated
Normal file
759
poetry.lock
generated
Normal file
@@ -0,0 +1,759 @@
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.0"
|
||||
description = "Atomic file writes."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "21.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[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"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "autopep8"
|
||||
version = "1.5.7"
|
||||
description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pycodestyle = ">=2.7.0"
|
||||
toml = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "4.2.4"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "~=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.6"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "dataclasses"
|
||||
version = "0.8"
|
||||
description = "A backport of the dataclasses module for Python 3.6"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6, <3.7"
|
||||
|
||||
[[package]]
|
||||
name = "fire"
|
||||
version = "0.4.0"
|
||||
description = "A library for automatically generating command line interfaces."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
termcolor = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "3.9.2"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
mccabe = ">=0.6.0,<0.7.0"
|
||||
pycodestyle = ">=2.7.0,<2.8.0"
|
||||
pyflakes = ">=2.3.0,<2.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.1.0"
|
||||
description = "Google API client core library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
google-auth = ">=1.25.0,<3.0dev"
|
||||
googleapis-common-protos = ">=1.6.0,<2.0dev"
|
||||
protobuf = ">=3.12.0"
|
||||
requests = ">=2.18.0,<3.0.0dev"
|
||||
|
||||
[package.extras]
|
||||
grpc = ["grpcio (>=1.33.2,<2.0dev)"]
|
||||
grpcgcp = ["grpcio-gcp (>=0.2.2)"]
|
||||
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.23.0"
|
||||
description = "Google API Client Library for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = ">=1.21.0,<3.0.0dev"
|
||||
google-auth = ">=1.16.0,<3.0.0dev"
|
||||
google-auth-httplib2 = ">=0.1.0"
|
||||
httplib2 = ">=0.15.0,<1dev"
|
||||
uritemplate = ">=3.0.0,<4dev"
|
||||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.2.1"
|
||||
description = "Google Authentication Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">= 3.6"
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=2.0.0,<5.0"
|
||||
pyasn1-modules = ">=0.2.1"
|
||||
rsa = ">=3.1.4,<5"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"]
|
||||
pyopenssl = ["pyopenssl (>=20.0.0)"]
|
||||
reauth = ["pyu2f (>=0.1.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-httplib2"
|
||||
version = "0.1.0"
|
||||
description = "Google Authentication Library: httplib2 transport"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
google-auth = "*"
|
||||
httplib2 = ">=0.15.0"
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.53.0"
|
||||
description = "Common protobufs used in Google APIs"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
protobuf = ">=3.12.0"
|
||||
|
||||
[package.extras]
|
||||
grpc = ["grpcio (>=1.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httplib2"
|
||||
version = "0.20.1"
|
||||
description = "A comprehensive HTTP client library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.4.2,<3"
|
||||
|
||||
[[package]]
|
||||
name = "icalendar"
|
||||
version = "4.0.7"
|
||||
description = "iCalendar parser/generator"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = "*"
|
||||
pytz = "*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.2"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "4.8.1"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||
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]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.6.1"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.0"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2"
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "3.18.1"
|
||||
description = "Protocol Buffers"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.10.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.4.8"
|
||||
description = "ASN.1 types and codecs"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.2.8"
|
||||
description = "A collection of ASN.1-based protocols modules."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.4.6,<0.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.7.0"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.8.2"
|
||||
description = "Data validation and settings management using python 3.6 type hinting"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1"
|
||||
|
||||
[package.dependencies]
|
||||
dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
|
||||
typing-extensions = ">=3.7.4.3"
|
||||
|
||||
[package.extras]
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "2.3.1"
|
||||
description = "passive checker of Python programs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "2.4.7"
|
||||
description = "Python parsing module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "6.2.5"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
toml = "*"
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2021.1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "5.4.1"
|
||||
description = "YAML parser and emitter for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.26.0"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.7.2"
|
||||
description = "Pure-Python RSA implementation"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5, <4"
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.0"
|
||||
description = "ANSII Color formatting for output in terminal."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.10.0.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "uritemplate"
|
||||
version = "3.0.1"
|
||||
description = "URI templates"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.7"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.6.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
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)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.6.1"
|
||||
content-hash = "9145569d3597a35a93b05b114ac7bcdba29e1b1fb215793b427b0e40fbe52bef"
|
||||
|
||||
[metadata.files]
|
||||
atomicwrites = [
|
||||
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
||||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
|
||||
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
||||
]
|
||||
autopep8 = [
|
||||
{file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"},
|
||||
{file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"},
|
||||
]
|
||||
cachetools = [
|
||||
{file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"},
|
||||
{file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
|
||||
{file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
dataclasses = [
|
||||
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
|
||||
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
|
||||
]
|
||||
fire = [
|
||||
{file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"},
|
||||
]
|
||||
flake8 = [
|
||||
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
|
||||
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
|
||||
]
|
||||
google-api-core = [
|
||||
{file = "google-api-core-2.1.0.tar.gz", hash = "sha256:5ec27b942b34d04559cbf3674430bb83fc3d74e7d32b8bbd31c4466e71740b83"},
|
||||
{file = "google_api_core-2.1.0-py2.py3-none-any.whl", hash = "sha256:c344e1aacd8330527c5130bdfe03118d8859ce798bcf0e5d23770ab6873e0615"},
|
||||
]
|
||||
google-api-python-client = [
|
||||
{file = "google-api-python-client-2.23.0.tar.gz", hash = "sha256:f117a595717fc384446f6235019e6a83fc9df821bd9d05dba7ff14aa96c70f52"},
|
||||
{file = "google_api_python_client-2.23.0-py2.py3-none-any.whl", hash = "sha256:a7b364eff63ca75d827cfb241a0f8567157976e879046c1ff20ddf735bad618e"},
|
||||
]
|
||||
google-auth = [
|
||||
{file = "google-auth-2.2.1.tar.gz", hash = "sha256:6dc8173abd50f25b6e62fc5b42802c96fc7cd9deb9bfeeb10a79f5606225cdf4"},
|
||||
{file = "google_auth-2.2.1-py2.py3-none-any.whl", hash = "sha256:2a92b485afed5292946b324e91fcbe03db277ee4cb64c998c6cfa66d4af01dee"},
|
||||
]
|
||||
google-auth-httplib2 = [
|
||||
{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"},
|
||||
]
|
||||
googleapis-common-protos = [
|
||||
{file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"},
|
||||
{file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"},
|
||||
]
|
||||
httplib2 = [
|
||||
{file = "httplib2-0.20.1-py3-none-any.whl", hash = "sha256:8fa4dbf2fbf839b71f8c7837a831e00fcdc860feca99b8bda58ceae4bc53d185"},
|
||||
{file = "httplib2-0.20.1.tar.gz", hash = "sha256:0efbcb8bfbfbc11578130d87d8afcc65c2274c6eb446e59fc674e4d7c972d327"},
|
||||
]
|
||||
icalendar = [
|
||||
{file = "icalendar-4.0.7-py2.py3-none-any.whl", hash = "sha256:8c35be16c1d0581a276002af883297aeffa8116e366fdce4d5318e1424aa1903"},
|
||||
{file = "icalendar-4.0.7.tar.gz", hash = "sha256:0fc18d87f66e0b5da84fa731389496cfe18e4c21304e8f6713556b2e8724a7a4"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
|
||||
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
|
||||
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
|
||||
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
protobuf = [
|
||||
{file = "protobuf-3.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fa6d1049d5315566f55c04d0b50c0033415144f96a9d25c820dc542fe2bb7f45"},
|
||||
{file = "protobuf-3.18.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e2790c580070cff2921b93d562539ae027064340151c50db6aaf94c33048cd"},
|
||||
{file = "protobuf-3.18.1-cp36-cp36m-win32.whl", hash = "sha256:7e2f0677d68ecdd1cfda2abea65873f5bc7c3f5aae199404a3f5c1d1198c1a63"},
|
||||
{file = "protobuf-3.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6f714f5de9d40b3bec90ede4a688cce52f637ccdc5403afcda1f67598f4fdcd7"},
|
||||
{file = "protobuf-3.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a7be937c319146cc9f2626f0181e6809062c353e1fe449ecd0df374ba1036b2"},
|
||||
{file = "protobuf-3.18.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:10544fc7ace885a882623083c24da5b14148c77563acddc3c58d66f6153c09cd"},
|
||||
{file = "protobuf-3.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ee8b11e3eb2ed38f12137c3c132270a0b1dd509e317228ac47b67f21a583f1"},
|
||||
{file = "protobuf-3.18.1-cp37-cp37m-win32.whl", hash = "sha256:c492c217d3f69f4d2d5619571e52ab98538edbf53caf67e53ea92bd0a3b5670f"},
|
||||
{file = "protobuf-3.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3c1644f8a7f19b45c7a4c32278e2a55ae9e7e2f9e5f02d960a61f04a4890d3e6"},
|
||||
{file = "protobuf-3.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9ac691f7b24e4371dcd3980e4f5d6c840a2010da37986203053fee995786ec5"},
|
||||
{file = "protobuf-3.18.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:93bad12895d8b0ebc66b605c2ef1802311595f881aef032d9f13282b7550e6b2"},
|
||||
{file = "protobuf-3.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0851b5b89191e1976d34fa2e8eb8659829dfb45252053224cf9df857fb5f6a45"},
|
||||
{file = "protobuf-3.18.1-cp38-cp38-win32.whl", hash = "sha256:09d9268f6f9da81b7657adcf2fb397524c82f20cdf9e0db3ff4e7567977abd67"},
|
||||
{file = "protobuf-3.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6d927774c0ec746fed15a4faff5f44aad0b7a3421fadb6f3ae5ca1f2f8ae26e"},
|
||||
{file = "protobuf-3.18.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d19c9cb805fd2be1d59eee39e152367ee92a30167e77bd06c8819f8f0009a4c"},
|
||||
{file = "protobuf-3.18.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:387f621bf7295a331f8c8a6962d097ceddeb85356792888cfa6a5c6bfc6886a4"},
|
||||
{file = "protobuf-3.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1c5d3966c856f60a9d8d62f4455d70c31026422acdd5c228edf22b65b16c38"},
|
||||
{file = "protobuf-3.18.1-cp39-cp39-win32.whl", hash = "sha256:f20f803892f2135e8b96dc58c9a0c6a7ad8436794bf8784af229498d939b4c77"},
|
||||
{file = "protobuf-3.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:d76201380f41a2d83fb613a4683059d1fcafbe969518b3e409e279a8788fde2f"},
|
||||
{file = "protobuf-3.18.1-py2.py3-none-any.whl", hash = "sha256:61ca58e14033ca0dfa484a31d57237c1be3b6013454c7f53876a20fc88dd69b1"},
|
||||
{file = "protobuf-3.18.1.tar.gz", hash = "sha256:1c9bb40503751087300dd12ce2e90899d68628977905c76effc48e66d089391e"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
|
||||
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
|
||||
]
|
||||
pyasn1 = [
|
||||
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
|
||||
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
|
||||
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
|
||||
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
|
||||
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
|
||||
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
|
||||
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
|
||||
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
|
||||
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
|
||||
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
|
||||
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
|
||||
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
|
||||
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
|
||||
]
|
||||
pyasn1-modules = [
|
||||
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
|
||||
]
|
||||
pycodestyle = [
|
||||
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
|
||||
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
|
||||
]
|
||||
pydantic = [
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
|
||||
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
|
||||
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
|
||||
]
|
||||
pyflakes = [
|
||||
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
|
||||
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
|
||||
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
|
||||
]
|
||||
python-dateutil = [
|
||||
{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"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
|
||||
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
|
||||
]
|
||||
pyyaml = [
|
||||
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
|
||||
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
]
|
||||
rsa = [
|
||||
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
|
||||
{file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
termcolor = [
|
||||
{file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||
{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 = [
|
||||
{file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"},
|
||||
{file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
|
||||
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
|
||||
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
|
||||
]
|
||||
@@ -1,3 +1,42 @@
|
||||
[tool.poetry]
|
||||
name = "sync_ics2gcal"
|
||||
version = "0.1.3"
|
||||
description = "Sync ics file with Google calendar"
|
||||
authors = ["Dmitry Belyaev <b4tm4n@mail.ru>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/b4tman/sync_ics2gcal"
|
||||
repository = "https://github.com/b4tman/sync_ics2gcal"
|
||||
keywords = ["icalendar", "sync", "google", "calendar"]
|
||||
classifiers = [
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.6.1"
|
||||
google-auth = "2.2.1"
|
||||
google-api-python-client = "2.23.0"
|
||||
icalendar = "4.0.7"
|
||||
pytz = "2021.1"
|
||||
PyYAML = "5.4.1"
|
||||
fire = "0.4.0"
|
||||
pydantic = "^1.8.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.2.5"
|
||||
flake8 = "^3.9.2"
|
||||
autopep8 = "^1.5.7"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
sync-ics2gcal = "sync_ics2gcal.sync_calendar:main"
|
||||
manage-ics2gcal = "sync_ics2gcal.manage_calendars:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=40.8.0", "wheel", "setuptools_scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
google-auth==1.11.0
|
||||
google-api-python-client==1.7.11
|
||||
icalendar==4.0.4
|
||||
pytz==2019.3
|
||||
PyYAML==5.3
|
||||
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
|
||||
@@ -1,7 +0,0 @@
|
||||
[metadata]
|
||||
license_files = LICENSE
|
||||
|
||||
[options]
|
||||
setup_requires =
|
||||
setuptools_scm
|
||||
setuptools_scm_git_archive
|
||||
44
setup.py
44
setup.py
@@ -1,44 +0,0 @@
|
||||
import setuptools
|
||||
|
||||
with open('README.md', 'r') as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name='sync-ics2gcal',
|
||||
author='Dmitry Belyaev',
|
||||
author_email='b4tm4n@mail.ru',
|
||||
license='MIT',
|
||||
description='Sync ics file with Google calendar',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/b4tman/sync_ics2gcal',
|
||||
use_scm_version={
|
||||
'fallback_version': '0.1',
|
||||
'local_scheme': 'no-local-version'
|
||||
},
|
||||
setup_requires=['setuptools_scm', 'setuptools_scm_git_archive'],
|
||||
packages=setuptools.find_packages(exclude=['tests']),
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
],
|
||||
python_requires='>=3.5',
|
||||
install_requires = [
|
||||
'google-auth>=1.5.0',
|
||||
'google-api-python-client>=1.7.0',
|
||||
'icalendar>=4.0.1',
|
||||
'pytz',
|
||||
'PyYAML>=3.13'
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"sync-ics2gcal = sync_ics2gcal.sync_calendar:main",
|
||||
"manage-ics2gcal = sync_ics2gcal.manage_calendars:main",
|
||||
]
|
||||
}
|
||||
)
|
||||
@@ -1,12 +1,16 @@
|
||||
|
||||
from .ical import (
|
||||
CalendarConverter,
|
||||
EventConverter
|
||||
EventConverter,
|
||||
DateDateTime
|
||||
)
|
||||
|
||||
from .gcal import (
|
||||
GoogleCalendarService,
|
||||
GoogleCalendar
|
||||
GoogleCalendar,
|
||||
EventData,
|
||||
EventList,
|
||||
EventTuple
|
||||
)
|
||||
|
||||
from .sync import (
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Callable, Tuple, Optional, Union
|
||||
|
||||
import google.auth
|
||||
from google.oauth2 import service_account
|
||||
from googleapiclient import discovery
|
||||
from pytz import utc
|
||||
|
||||
EventData = Dict[str, Union[str, 'EventData', None]]
|
||||
EventList = List[EventData]
|
||||
EventTuple = Tuple[EventData, EventData]
|
||||
|
||||
class GoogleCalendarService():
|
||||
|
||||
class GoogleCalendarService:
|
||||
"""class for make google calendar service Resource
|
||||
|
||||
Returns:
|
||||
@@ -19,51 +24,49 @@ class GoogleCalendarService():
|
||||
"""make service Resource from default credentials (authorize)
|
||||
( https://developers.google.com/identity/protocols/application-default-credentials )
|
||||
( https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default )
|
||||
|
||||
Returns:
|
||||
service Resource
|
||||
"""
|
||||
|
||||
scopes = ['https://www.googleapis.com/auth/calendar']
|
||||
credentials, _ = google.auth.default(scopes=scopes)
|
||||
service = discovery.build('calendar', 'v3', credentials=credentials)
|
||||
service = discovery.build(
|
||||
'calendar', 'v3', credentials=credentials, cache_discovery=False)
|
||||
return service
|
||||
|
||||
@staticmethod
|
||||
def from_srv_acc_file(service_account_file):
|
||||
def from_srv_acc_file(service_account_file: str):
|
||||
"""make service Resource from service account filename (authorize)
|
||||
|
||||
Returns:
|
||||
service Resource
|
||||
"""
|
||||
|
||||
scopes = ['https://www.googleapis.com/auth/calendar']
|
||||
credentials = service_account.Credentials.from_service_account_file(service_account_file)
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
service_account_file)
|
||||
scoped_credentials = credentials.with_scopes(scopes)
|
||||
service = discovery.build('calendar', 'v3', credentials=scoped_credentials)
|
||||
service = discovery.build(
|
||||
'calendar', 'v3', credentials=scoped_credentials,
|
||||
cache_discovery=False)
|
||||
return service
|
||||
|
||||
@staticmethod
|
||||
def from_config(config):
|
||||
def from_config(config: Optional[Dict[str, Optional[str]]] = None):
|
||||
"""make service Resource from config dict
|
||||
|
||||
Arguments:
|
||||
config -- dict() config with keys:
|
||||
config -- config with keys:
|
||||
(optional) service_account: - service account filename
|
||||
if key not in dict then default credentials will be used
|
||||
( https://developers.google.com/identity/protocols/application-default-credentials )
|
||||
|
||||
Returns:
|
||||
service Resource
|
||||
-- None: default credentials will be used
|
||||
"""
|
||||
|
||||
if 'service_account' in config:
|
||||
service = GoogleCalendarService.from_srv_acc_file(config['service_account'])
|
||||
if config is not None and 'service_account' in config:
|
||||
service = GoogleCalendarService.from_srv_acc_file(
|
||||
config['service_account'])
|
||||
else:
|
||||
service = GoogleCalendarService.default()
|
||||
return service
|
||||
|
||||
def select_event_key(event):
|
||||
|
||||
def select_event_key(event: EventData) -> Optional[str]:
|
||||
"""select event key for logging
|
||||
|
||||
Arguments:
|
||||
@@ -81,17 +84,17 @@ def select_event_key(event):
|
||||
return key
|
||||
|
||||
|
||||
class GoogleCalendar():
|
||||
class GoogleCalendar:
|
||||
"""class to interact with calendar on google
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('GoogleCalendar')
|
||||
|
||||
def __init__(self, service, calendarId):
|
||||
self.service = service
|
||||
self.calendarId = calendarId
|
||||
def __init__(self, service: discovery.Resource, calendarId: Optional[str]):
|
||||
self.service: discovery.Resource = service
|
||||
self.calendarId: str = calendarId
|
||||
|
||||
def _make_request_callback(self, action, events_by_req):
|
||||
def _make_request_callback(self, action: str, events_by_req: EventList) -> Callable:
|
||||
"""make callback for log result of batch request
|
||||
|
||||
Arguments:
|
||||
@@ -107,8 +110,10 @@ class GoogleCalendar():
|
||||
key = select_event_key(event)
|
||||
|
||||
if exception is not None:
|
||||
self.logger.error('failed to %s event with %s: %s, exception: %s',
|
||||
action, key, event.get(key), str(exception))
|
||||
self.logger.error(
|
||||
'failed to %s event with %s: %s, exception: %s',
|
||||
action, key, event.get(key), str(exception)
|
||||
)
|
||||
else:
|
||||
resp_key = select_event_key(response)
|
||||
if resp_key is not None:
|
||||
@@ -116,9 +121,10 @@ class GoogleCalendar():
|
||||
key = resp_key
|
||||
self.logger.info('event %s ok, %s: %s',
|
||||
action, key, event.get(key))
|
||||
|
||||
return callback
|
||||
|
||||
def list_events_from(self, start):
|
||||
def list_events_from(self, start: datetime) -> EventList:
|
||||
""" list events from calendar, where start date >= start
|
||||
"""
|
||||
fields = 'nextPageToken,items(id,iCalUID,updated)'
|
||||
@@ -127,8 +133,11 @@ class GoogleCalendar():
|
||||
timeMin = utc.normalize(start.astimezone(utc)).replace(
|
||||
tzinfo=None).isoformat() + 'Z'
|
||||
while True:
|
||||
response = self.service.events().list(calendarId=self.calendarId, pageToken=page_token,
|
||||
singleEvents=True, timeMin=timeMin, fields=fields).execute()
|
||||
response = self.service.events().list(calendarId=self.calendarId,
|
||||
pageToken=page_token,
|
||||
singleEvents=True,
|
||||
timeMin=timeMin,
|
||||
fields=fields).execute()
|
||||
if 'items' in response:
|
||||
events.extend(response['items'])
|
||||
page_token = response.get('nextPageToken')
|
||||
@@ -137,7 +146,7 @@ class GoogleCalendar():
|
||||
self.logger.info('%d events listed', len(events))
|
||||
return events
|
||||
|
||||
def find_exists(self, events):
|
||||
def find_exists(self, events: List) -> Tuple[List[EventTuple], EventList]:
|
||||
""" find existing events from list, by 'iCalUID' field
|
||||
|
||||
Arguments:
|
||||
@@ -155,15 +164,16 @@ class GoogleCalendar():
|
||||
|
||||
def list_callback(request_id, response, exception):
|
||||
found = False
|
||||
event = events_by_req[int(request_id)]
|
||||
cur_event = events_by_req[int(request_id)]
|
||||
if exception is None:
|
||||
found = ([] != response['items'])
|
||||
else:
|
||||
self.logger.error('exception %s, while listing event with UID: %s', str(
|
||||
exception), event['iCalUID'])
|
||||
self.logger.error(
|
||||
'exception %s, while listing event with UID: %s',
|
||||
str(exception), cur_event['iCalUID'])
|
||||
if found:
|
||||
exists.append(
|
||||
(event, response['items'][0]))
|
||||
(cur_event, response['items'][0]))
|
||||
else:
|
||||
not_found.append(events_by_req[int(request_id)])
|
||||
|
||||
@@ -172,14 +182,19 @@ class GoogleCalendar():
|
||||
for event in events:
|
||||
events_by_req.append(event)
|
||||
batch.add(self.service.events().list(calendarId=self.calendarId,
|
||||
iCalUID=event['iCalUID'], showDeleted=True, fields=fields), request_id=str(i))
|
||||
iCalUID=event['iCalUID'],
|
||||
showDeleted=True,
|
||||
fields=fields
|
||||
),
|
||||
request_id=str(i)
|
||||
)
|
||||
i += 1
|
||||
batch.execute()
|
||||
self.logger.info('%d events exists, %d not found',
|
||||
len(exists), len(not_found))
|
||||
return exists, not_found
|
||||
|
||||
def insert_events(self, events):
|
||||
def insert_events(self, events: EventList):
|
||||
""" insert list of events
|
||||
|
||||
Arguments:
|
||||
@@ -195,11 +210,13 @@ class GoogleCalendar():
|
||||
for event in events:
|
||||
events_by_req.append(event)
|
||||
batch.add(self.service.events().insert(
|
||||
calendarId=self.calendarId, body=event, fields=fields), request_id=str(i))
|
||||
calendarId=self.calendarId, body=event, fields=fields),
|
||||
request_id=str(i)
|
||||
)
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def patch_events(self, event_tuples):
|
||||
def patch_events(self, event_tuples: List[EventTuple]):
|
||||
""" patch (update) events
|
||||
|
||||
Arguments:
|
||||
@@ -217,11 +234,12 @@ class GoogleCalendar():
|
||||
continue
|
||||
events_by_req.append(event_new)
|
||||
batch.add(self.service.events().patch(
|
||||
calendarId=self.calendarId, eventId=event_old['id'], body=event_new), fields=fields, request_id=str(i))
|
||||
calendarId=self.calendarId, eventId=event_old['id'],
|
||||
body=event_new), fields=fields, request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def update_events(self, event_tuples):
|
||||
def update_events(self, event_tuples: List[EventTuple]):
|
||||
""" update events
|
||||
|
||||
Arguments:
|
||||
@@ -239,11 +257,12 @@ class GoogleCalendar():
|
||||
continue
|
||||
events_by_req.append(event_new)
|
||||
batch.add(self.service.events().update(
|
||||
calendarId=self.calendarId, eventId=event_old['id'], body=event_new, fields=fields), request_id=str(i))
|
||||
calendarId=self.calendarId, eventId=event_old['id'],
|
||||
body=event_new, fields=fields), request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def delete_events(self, events):
|
||||
def delete_events(self, events: EventList):
|
||||
""" delete events
|
||||
|
||||
Arguments:
|
||||
@@ -258,11 +277,12 @@ class GoogleCalendar():
|
||||
for event in events:
|
||||
events_by_req.append(event)
|
||||
batch.add(self.service.events().delete(
|
||||
calendarId=self.calendarId, eventId=event['id']), request_id=str(i))
|
||||
calendarId=self.calendarId,
|
||||
eventId=event['id']), request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def create(self, summary, timeZone=None):
|
||||
def create(self, summary: str, timeZone: Optional[str] = None) -> Any:
|
||||
"""create calendar
|
||||
|
||||
Arguments:
|
||||
@@ -279,7 +299,9 @@ class GoogleCalendar():
|
||||
if timeZone is not None:
|
||||
calendar['timeZone'] = timeZone
|
||||
|
||||
created_calendar = self.service.calendars().insert(body=calendar).execute()
|
||||
created_calendar = self.service.calendars().insert(
|
||||
body=calendar
|
||||
).execute()
|
||||
self.calendarId = created_calendar['id']
|
||||
return created_calendar
|
||||
|
||||
@@ -299,9 +321,12 @@ class GoogleCalendar():
|
||||
},
|
||||
'role': 'reader'
|
||||
}
|
||||
return self.service.acl().insert(calendarId=self.calendarId, body=rule_public).execute()
|
||||
return self.service.acl().insert(
|
||||
calendarId=self.calendarId,
|
||||
body=rule_public
|
||||
).execute()
|
||||
|
||||
def add_owner(self, email):
|
||||
def add_owner(self, email: str):
|
||||
"""add calendar owner by email
|
||||
|
||||
Arguments:
|
||||
@@ -315,4 +340,7 @@ class GoogleCalendar():
|
||||
},
|
||||
'role': 'owner'
|
||||
}
|
||||
return self.service.acl().insert(calendarId=self.calendarId, body=rule_owner).execute()
|
||||
return self.service.acl().insert(
|
||||
calendarId=self.calendarId,
|
||||
body=rule_owner
|
||||
).execute()
|
||||
|
||||
@@ -1,39 +1,96 @@
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Union, Dict, Callable, Optional
|
||||
|
||||
from icalendar import Calendar, Event
|
||||
from pytz import utc
|
||||
|
||||
import pydantic
|
||||
|
||||
def format_datetime_utc(value):
|
||||
from .gcal import EventData, EventList
|
||||
|
||||
DateDateTime = Union[datetime.date, datetime.datetime]
|
||||
|
||||
|
||||
class GCal_DateDateTime(pydantic.BaseModel):
|
||||
date: Optional[str] = pydantic.Field(default=None)
|
||||
date_time: Optional[str] = pydantic.Field(alias='dateTime', default=None)
|
||||
timezone: Optional[str] = pydantic.Field(alias='timeZone', default=None)
|
||||
|
||||
@pydantic.root_validator(allow_reuse=True)
|
||||
def check_only_date_or_datetime(cls, values):
|
||||
date = values.get('date', None)
|
||||
date_time = values.get('date_time', None)
|
||||
assert (date is None) != (date_time is None), \
|
||||
'only date or date_time must be provided'
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def create_from(cls, value: DateDateTime) -> 'GCal_DateDateTime':
|
||||
key: str = 'date'
|
||||
str_value: str = ''
|
||||
if type(value) is datetime.datetime:
|
||||
key = 'date_time'
|
||||
str_value = format_datetime_utc(value)
|
||||
else:
|
||||
str_value = value.isoformat()
|
||||
return cls(**{key: str_value})
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class GCal_Event(pydantic.BaseModel):
|
||||
created: Optional[str] = None
|
||||
updated: Optional[str] = None
|
||||
summary: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
start: GCal_DateDateTime
|
||||
end: GCal_DateDateTime
|
||||
transparency: Optional[str] = None
|
||||
ical_uid: str = pydantic.Field(alias='iCalUID')
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
def format_datetime_utc(value: DateDateTime) -> str:
|
||||
"""utc datetime as string from date or datetime value
|
||||
|
||||
Arguments:
|
||||
value -- date or datetime value
|
||||
|
||||
Returns:
|
||||
utc datetime value as string in iso format
|
||||
"""
|
||||
if not isinstance(value, datetime.datetime):
|
||||
if type(value) is datetime.date:
|
||||
value = datetime.datetime(
|
||||
value.year, value.month, value.day, tzinfo=utc)
|
||||
value = value.replace(microsecond=1)
|
||||
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(value, check_value=None):
|
||||
def gcal_date_or_dateTime(value: DateDateTime,
|
||||
check_value: Optional[DateDateTime] = None) \
|
||||
-> Dict[str, str]:
|
||||
"""date or dateTime to gcal (start or end dict)
|
||||
|
||||
Arguments:
|
||||
value -- date or datetime value
|
||||
check_value - date or datetime to choise result type (if not None)
|
||||
value: date or datetime
|
||||
check_value: optional for choose result type
|
||||
|
||||
Returns:
|
||||
dict { 'date': ... } or { 'dateTime': ... }
|
||||
{ 'date': ... } or { 'dateTime': ... }
|
||||
"""
|
||||
|
||||
if check_value is None:
|
||||
check_value = value
|
||||
|
||||
result = {}
|
||||
result: Dict[str, str] = {}
|
||||
if isinstance(check_value, datetime.datetime):
|
||||
result['dateTime'] = format_datetime_utc(value)
|
||||
else:
|
||||
@@ -49,7 +106,7 @@ class EventConverter(Event):
|
||||
( https://developers.google.com/calendar/v3/reference/events#resource-representations )
|
||||
"""
|
||||
|
||||
def _str_prop(self, prop):
|
||||
def _str_prop(self, prop: str) -> str:
|
||||
"""decoded string property
|
||||
|
||||
Arguments:
|
||||
@@ -61,7 +118,7 @@ class EventConverter(Event):
|
||||
|
||||
return self.decoded(prop).decode(encoding='utf-8')
|
||||
|
||||
def _datetime_str_prop(self, prop):
|
||||
def _datetime_str_prop(self, prop: str) -> str:
|
||||
"""utc datetime as string from property
|
||||
|
||||
Arguments:
|
||||
@@ -73,7 +130,7 @@ class EventConverter(Event):
|
||||
|
||||
return format_datetime_utc(self.decoded(prop))
|
||||
|
||||
def _gcal_start(self):
|
||||
def _gcal_start(self) -> GCal_DateDateTime:
|
||||
""" event start dict from icalendar event
|
||||
|
||||
Raises:
|
||||
@@ -84,9 +141,9 @@ class EventConverter(Event):
|
||||
"""
|
||||
|
||||
value = self.decoded('DTSTART')
|
||||
return gcal_date_or_dateTime(value)
|
||||
return GCal_DateDateTime.create_from(value)
|
||||
|
||||
def _gcal_end(self):
|
||||
def _gcal_end(self) -> GCal_DateDateTime:
|
||||
"""event end dict from icalendar event
|
||||
|
||||
Raises:
|
||||
@@ -98,18 +155,24 @@ class EventConverter(Event):
|
||||
result = None
|
||||
if 'DTEND' in self:
|
||||
value = self.decoded('DTEND')
|
||||
result = gcal_date_or_dateTime(value)
|
||||
result = GCal_DateDateTime.create_from(value)
|
||||
elif 'DURATION' in self:
|
||||
start_val = self.decoded('DTSTART')
|
||||
duration = self.decoded('DURATION')
|
||||
end_val = start_val + duration
|
||||
if type(start_val) is datetime.date:
|
||||
if type(end_val) is datetime.datetime:
|
||||
end_val = datetime.date(
|
||||
end_val.year, end_val.month, end_val.day)
|
||||
|
||||
result = gcal_date_or_dateTime(end_val, check_value=start_val)
|
||||
result = GCal_DateDateTime.create_from(end_val)
|
||||
else:
|
||||
raise ValueError('no DTEND or DURATION')
|
||||
return result
|
||||
|
||||
def _put_to_gcal(self, gcal_event, prop, func, ics_prop=None):
|
||||
def _put_to_gcal(self, gcal_event: EventData,
|
||||
prop: str, func: Callable[[str], str],
|
||||
ics_prop: Optional[str] = None):
|
||||
"""get property from ical event if exist, and put to gcal event
|
||||
|
||||
Arguments:
|
||||
@@ -124,54 +187,62 @@ class EventConverter(Event):
|
||||
if ics_prop in self:
|
||||
gcal_event[prop] = func(ics_prop)
|
||||
|
||||
def to_gcal(self):
|
||||
def _get_prop(self, prop: str, func: Callable[[str], str]):
|
||||
"""get property from ical event if exist else None
|
||||
|
||||
Arguments:
|
||||
prop -- property name
|
||||
func -- function to convert
|
||||
"""
|
||||
|
||||
if prop not in self:
|
||||
return None
|
||||
return func(prop)
|
||||
|
||||
def to_gcal(self) -> EventData:
|
||||
"""Convert
|
||||
|
||||
Returns:
|
||||
dict - google calendar#event resource
|
||||
"""
|
||||
|
||||
event = {
|
||||
'iCalUID': self._str_prop('UID')
|
||||
kwargs = {
|
||||
'ical_uid': self._str_prop('UID'),
|
||||
'start': self._gcal_start(),
|
||||
'end': self._gcal_end(),
|
||||
'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()),
|
||||
}
|
||||
|
||||
event['start'] = self._gcal_start()
|
||||
event['end'] = self._gcal_end()
|
||||
|
||||
self._put_to_gcal(event, 'summary', self._str_prop)
|
||||
self._put_to_gcal(event, 'description', self._str_prop)
|
||||
self._put_to_gcal(event, 'location', self._str_prop)
|
||||
self._put_to_gcal(event, 'created', self._datetime_str_prop)
|
||||
self._put_to_gcal(
|
||||
event, 'updated', self._datetime_str_prop, 'LAST-MODIFIED')
|
||||
self._put_to_gcal(
|
||||
event, 'transparency', lambda prop: self._str_prop(prop).lower(), 'TRANSP')
|
||||
|
||||
return event
|
||||
return GCal_Event(**kwargs).dict(by_alias=True, exclude_defaults=True)
|
||||
|
||||
|
||||
class CalendarConverter():
|
||||
class CalendarConverter:
|
||||
"""Convert icalendar events to google calendar resources
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('CalendarConverter')
|
||||
|
||||
def __init__(self, calendar=None):
|
||||
self.calendar = calendar
|
||||
def __init__(self, calendar: Optional[Calendar] = None):
|
||||
self.calendar: Optional[Calendar] = calendar
|
||||
|
||||
def load(self, filename):
|
||||
def load(self, filename: str):
|
||||
""" load calendar from ics file
|
||||
"""
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
self.calendar = Calendar.from_ical(f.read())
|
||||
self.logger.info('%s loaded', filename)
|
||||
|
||||
def loads(self, string):
|
||||
def loads(self, string: str):
|
||||
""" load calendar from ics string
|
||||
"""
|
||||
self.calendar = Calendar.from_ical(string)
|
||||
|
||||
def events_to_gcal(self):
|
||||
def events_to_gcal(self) -> EventList:
|
||||
"""Convert events to google calendar resources
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,103 +1,144 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import logging.config
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
import fire
|
||||
import yaml
|
||||
from pytz import utc
|
||||
|
||||
from . import GoogleCalendar, GoogleCalendarService
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="manage google calendars in service account")
|
||||
command_subparsers = parser.add_subparsers(help='command', dest='command')
|
||||
command_subparsers.add_parser('list', help='list calendars')
|
||||
parser_create = command_subparsers.add_parser(
|
||||
'create', help='create calendar')
|
||||
parser_create.add_argument(
|
||||
'summary', action='store', help='new calendar summary')
|
||||
parser_create.add_argument('--timezone', action='store',
|
||||
default=None, required=False, help='new calendar timezone')
|
||||
parser_create.add_argument(
|
||||
'--public', default=False, action='store_true', help='make calendar public')
|
||||
parser_add_owner = command_subparsers.add_parser(
|
||||
'add_owner', help='add owner to calendar')
|
||||
parser_add_owner.add_argument('id', action='store', help='calendar id')
|
||||
parser_add_owner.add_argument(
|
||||
'owner_email', action='store', help='new owner email')
|
||||
parser_remove = command_subparsers.add_parser(
|
||||
'remove', help='remove calendar')
|
||||
parser_remove.add_argument(
|
||||
'id', action='store', help='calendar id to remove')
|
||||
parser_rename = command_subparsers.add_parser(
|
||||
'rename', help='rename calendar')
|
||||
parser_rename.add_argument(
|
||||
'id', action='store', help='calendar id')
|
||||
parser_rename.add_argument(
|
||||
'summary', action='store', help='new summary')
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command is None:
|
||||
parser.print_usage()
|
||||
return args
|
||||
|
||||
|
||||
def load_config():
|
||||
with open('config.yml', 'r', encoding='utf-8') as f:
|
||||
def load_config(filename: str) -> Optional[Dict[str, Any]]:
|
||||
result = None
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
result = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def list_calendars(service):
|
||||
response = service.calendarList().list(fields='items(id,summary)').execute()
|
||||
for calendar in response.get('items'):
|
||||
class PropertyCommands:
|
||||
""" get/set google calendar properties """
|
||||
|
||||
def __init__(self, _service):
|
||||
self._service = _service
|
||||
|
||||
def get(self, calendar_id: str, property_name: str) -> None:
|
||||
""" get calendar property
|
||||
|
||||
Args:
|
||||
calendar_id: calendar id
|
||||
property_name: property key
|
||||
"""
|
||||
response = self._service.calendarList().get(calendarId=calendar_id,
|
||||
fields=property_name).execute()
|
||||
print(response.get(property_name))
|
||||
|
||||
def set(self, calendar_id: str, property_name: str, property_value: str) -> None:
|
||||
""" set calendar property
|
||||
|
||||
Args:
|
||||
calendar_id: calendar id
|
||||
property_name: property key
|
||||
property_value: property value
|
||||
"""
|
||||
body = {property_name: property_value}
|
||||
response = self._service.calendarList().patch(body=body, calendarId=calendar_id).execute()
|
||||
print(response)
|
||||
|
||||
|
||||
class Commands:
|
||||
""" manage google calendars in service account """
|
||||
|
||||
def __init__(self, config: str = 'config.yml'):
|
||||
"""
|
||||
|
||||
Args:
|
||||
config(str): config filename
|
||||
"""
|
||||
self._config: Optional[Dict[str, Any]] = load_config(config)
|
||||
if self._config is not None and 'logging' in self._config:
|
||||
logging.config.dictConfig(self._config['logging'])
|
||||
self._service = GoogleCalendarService.from_config(self._config)
|
||||
self.property = PropertyCommands(self._service)
|
||||
|
||||
def list(self, show_hidden: bool = False, show_deleted: bool = False) -> None:
|
||||
""" list calendars
|
||||
|
||||
Args:
|
||||
show_hidden: show hidden calendars
|
||||
show_deleted: show deleted calendars
|
||||
"""
|
||||
|
||||
fields: str = 'nextPageToken,items(id,summary)'
|
||||
calendars: List[Dict[str, Any]] = []
|
||||
page_token: Optional[str] = None
|
||||
while True:
|
||||
calendars_api = self._service.calendarList()
|
||||
response = calendars_api.list(fields=fields,
|
||||
pageToken=page_token,
|
||||
showHidden=show_hidden,
|
||||
showDeleted=show_deleted
|
||||
).execute()
|
||||
if 'items' in response:
|
||||
calendars.extend(response['items'])
|
||||
page_token = response.get('nextPageToken')
|
||||
if page_token is None:
|
||||
break
|
||||
for calendar in calendars:
|
||||
print('{summary}: {id}'.format_map(calendar))
|
||||
|
||||
def create(self, summary: str, timezone: Optional[str] = None, public: bool = False) -> None:
|
||||
""" create calendar
|
||||
|
||||
def create_calendar(service, summary, timezone, public):
|
||||
calendar = GoogleCalendar(service, None)
|
||||
Args:
|
||||
summary: new calendar summary
|
||||
timezone: new calendar timezone
|
||||
public: make calendar public
|
||||
"""
|
||||
calendar = GoogleCalendar(self._service, None)
|
||||
calendar.create(summary, timezone)
|
||||
if public:
|
||||
calendar.make_public()
|
||||
print('{}: {}'.format(summary, calendar.calendarId))
|
||||
|
||||
def add_owner(self, calendar_id: str, email: str) -> None:
|
||||
""" add owner to calendar
|
||||
|
||||
def add_owner(service, id, owner_email):
|
||||
calendar = GoogleCalendar(service, id)
|
||||
calendar.add_owner(owner_email)
|
||||
print('to {} added owner: {}'.format(id, owner_email))
|
||||
Args:
|
||||
calendar_id: calendar id
|
||||
email: new owner email
|
||||
"""
|
||||
calendar = GoogleCalendar(self._service, calendar_id)
|
||||
calendar.add_owner(email)
|
||||
print('to {} added owner: {}'.format(calendar_id, email))
|
||||
|
||||
def remove(self, calendar_id: str) -> None:
|
||||
""" remove calendar
|
||||
|
||||
def remove_calendar(service, id):
|
||||
calendar = GoogleCalendar(service, id)
|
||||
Args:
|
||||
calendar_id: calendar id
|
||||
"""
|
||||
calendar = GoogleCalendar(self._service, calendar_id)
|
||||
calendar.delete()
|
||||
print('removed: {}'.format(id))
|
||||
print('removed: {}'.format(calendar_id))
|
||||
|
||||
def rename_calendar(service, id, summary):
|
||||
def rename(self, calendar_id: str, summary: str) -> None:
|
||||
""" rename calendar
|
||||
|
||||
Args:
|
||||
calendar_id: calendar id
|
||||
summary:
|
||||
"""
|
||||
calendar = {'summary': summary}
|
||||
service.calendars().patch(body=calendar, calendarId=id).execute()
|
||||
print('{}: {}'.format(summary, id))
|
||||
self._service.calendars().patch(body=calendar, calendarId=calendar_id).execute()
|
||||
print('{}: {}'.format(summary, calendar_id))
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config = load_config()
|
||||
fire.Fire(Commands, name='manage-ics2gcal')
|
||||
|
||||
if 'logging' in config:
|
||||
logging.config.dictConfig(config['logging'])
|
||||
|
||||
service = GoogleCalendarService.from_config(config)
|
||||
|
||||
if 'list' == args.command:
|
||||
list_calendars(service)
|
||||
elif 'create' == args.command:
|
||||
create_calendar(service, args.summary, args.timezone, args.public)
|
||||
elif 'add_owner' == args.command:
|
||||
add_owner(service, args.id, args.owner_email)
|
||||
elif 'remove' == args.command:
|
||||
remove_calendar(service, args.id)
|
||||
elif 'rename' == args.command:
|
||||
rename_calendar(service, args.id, args.summary)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import logging
|
||||
import operator
|
||||
from typing import List, Dict, Set, Tuple, Union, Callable
|
||||
|
||||
import dateutil.parser
|
||||
from pytz import utc
|
||||
|
||||
from .gcal import GoogleCalendar, EventData, EventList, EventTuple
|
||||
from .ical import CalendarConverter, DateDateTime
|
||||
|
||||
class CalendarSync():
|
||||
|
||||
class CalendarSync:
|
||||
"""class for syncronize calendar with google
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('CalendarSync')
|
||||
|
||||
def __init__(self, gcalendar, converter):
|
||||
self.gcalendar = gcalendar
|
||||
self.converter = converter
|
||||
def __init__(self, gcalendar: GoogleCalendar, converter: CalendarConverter):
|
||||
self.gcalendar: GoogleCalendar = gcalendar
|
||||
self.converter: CalendarConverter = converter
|
||||
self.to_insert: EventList = []
|
||||
self.to_update: List[EventTuple] = []
|
||||
self.to_delete: EventList = []
|
||||
|
||||
@staticmethod
|
||||
def _events_list_compare(items_src, items_dst, key='iCalUID'):
|
||||
def _events_list_compare(items_src: EventList,
|
||||
items_dst: EventList,
|
||||
key: str = 'iCalUID') \
|
||||
-> Tuple[EventList, List[EventTuple], EventList]:
|
||||
""" compare list of events by key
|
||||
|
||||
Arguments:
|
||||
@@ -30,16 +41,18 @@ class CalendarSync():
|
||||
items_to_delete)
|
||||
"""
|
||||
|
||||
def get_key(item): return item[key]
|
||||
def get_key(item: EventData) -> str: return item[key]
|
||||
|
||||
keys_src = set(map(get_key, items_src))
|
||||
keys_dst = set(map(get_key, items_dst))
|
||||
keys_src: Set[str] = set(map(get_key, items_src))
|
||||
keys_dst: Set[str] = set(map(get_key, items_dst))
|
||||
|
||||
keys_to_insert = keys_src - keys_dst
|
||||
keys_to_update = keys_src & keys_dst
|
||||
keys_to_delete = keys_dst - keys_src
|
||||
|
||||
def items_by_keys(items, key_name, keys):
|
||||
def items_by_keys(items: EventList,
|
||||
key_name: str,
|
||||
keys: Set[str]) -> EventList:
|
||||
return list(filter(lambda item: item[key_name] in keys, items))
|
||||
|
||||
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
|
||||
@@ -57,14 +70,19 @@ class CalendarSync():
|
||||
""" filter 'to_update' events by 'updated' datetime
|
||||
"""
|
||||
|
||||
def filter_updated(event_tuple):
|
||||
def filter_updated(event_tuple: EventTuple) -> bool:
|
||||
new, old = event_tuple
|
||||
return dateutil.parser.parse(new['updated']) > dateutil.parser.parse(old['updated'])
|
||||
new_date = dateutil.parser.parse(new['updated'])
|
||||
old_date = dateutil.parser.parse(old['updated'])
|
||||
return new_date > old_date
|
||||
|
||||
self.to_update = list(filter(filter_updated, self.to_update))
|
||||
|
||||
@staticmethod
|
||||
def _filter_events_by_date(events, date, op):
|
||||
def _filter_events_by_date(events: EventList,
|
||||
date: DateDateTime,
|
||||
op: Callable[[DateDateTime,
|
||||
DateDateTime], bool]) -> EventList:
|
||||
""" filter events by start datetime
|
||||
|
||||
Arguments:
|
||||
@@ -76,10 +94,10 @@ class CalendarSync():
|
||||
list of filtred events
|
||||
"""
|
||||
|
||||
def filter_by_date(event):
|
||||
def filter_by_date(event: EventData) -> bool:
|
||||
date_cmp = date
|
||||
event_start = event['start']
|
||||
event_date = None
|
||||
event_start: Dict[str, str] = event['start']
|
||||
event_date: Union[DateDateTime, str, None] = None
|
||||
compare_dates = False
|
||||
|
||||
if 'date' in event_start:
|
||||
@@ -91,14 +109,15 @@ class CalendarSync():
|
||||
event_date = dateutil.parser.parse(event_date)
|
||||
if compare_dates:
|
||||
date_cmp = datetime.date(date.year, date.month, date.day)
|
||||
event_date = datetime.date(event_date.year, event_date.month, event_date.day)
|
||||
event_date = datetime.date(
|
||||
event_date.year, event_date.month, event_date.day)
|
||||
|
||||
return op(event_date, date_cmp)
|
||||
|
||||
return list(filter(filter_by_date, events))
|
||||
|
||||
@staticmethod
|
||||
def _tz_aware_datetime(date):
|
||||
def _tz_aware_datetime(date: DateDateTime) -> datetime.datetime:
|
||||
"""make tz aware datetime from datetime/date (utc if no tzinfo)
|
||||
|
||||
Arguments:
|
||||
@@ -114,7 +133,7 @@ class CalendarSync():
|
||||
date = date.replace(tzinfo=utc)
|
||||
return date
|
||||
|
||||
def prepare_sync(self, start_date):
|
||||
def prepare_sync(self, start_date: DateDateTime) -> None:
|
||||
"""prepare sync lists by comparsion of events
|
||||
|
||||
Arguments:
|
||||
@@ -132,42 +151,45 @@ class CalendarSync():
|
||||
events_src_past = CalendarSync._filter_events_by_date(
|
||||
events_src, start_date, operator.lt)
|
||||
|
||||
events_src = None
|
||||
|
||||
# first events comparsion
|
||||
self.to_insert, self.to_update, self.to_delete = CalendarSync._events_list_compare(
|
||||
events_src_pending, events_dst)
|
||||
|
||||
events_src_pending, events_dst = None, None
|
||||
|
||||
# find in events 'to_delete' past events from source, for update (move to past)
|
||||
_, add_to_update, self.to_delete = CalendarSync._events_list_compare(
|
||||
events_src_past, self.to_delete)
|
||||
self.to_update.extend(add_to_update)
|
||||
|
||||
events_src_past = None
|
||||
|
||||
# find if events 'to_insert' exists in gcalendar, for update them
|
||||
add_to_update, self.to_insert = self.gcalendar.find_exists(
|
||||
self.to_insert)
|
||||
self.to_update.extend(add_to_update)
|
||||
|
||||
add_to_update = None
|
||||
|
||||
# exclude outdated events from 'to_update' list, by 'updated' field
|
||||
self._filter_events_to_update()
|
||||
|
||||
self.logger.info('prepared to sync: ( insert: %d, update: %d, delete: %d )',
|
||||
len(self.to_insert), len(self.to_update), len(self.to_delete))
|
||||
self.logger.info(
|
||||
'prepared to sync: ( insert: %d, update: %d, delete: %d )',
|
||||
len(self.to_insert),
|
||||
len(self.to_update),
|
||||
len(self.to_delete)
|
||||
)
|
||||
|
||||
def apply(self):
|
||||
"""apply sync (insert, update, delete), using prepared lists of events
|
||||
def clear(self) -> None:
|
||||
""" clear prepared sync lists (insert, update, delete)
|
||||
"""
|
||||
self.to_insert.clear()
|
||||
self.to_update.clear()
|
||||
self.to_delete.clear()
|
||||
|
||||
def apply(self) -> None:
|
||||
""" apply sync (insert, update, delete), using prepared lists of events
|
||||
"""
|
||||
|
||||
self.gcalendar.insert_events(self.to_insert)
|
||||
self.gcalendar.update_events(self.to_update)
|
||||
self.gcalendar.delete_events(self.to_delete)
|
||||
|
||||
self.logger.info('sync done')
|
||||
self.clear()
|
||||
|
||||
self.to_insert, self.to_update, self.to_delete = [], [], []
|
||||
self.logger.info('sync done')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
import yaml
|
||||
|
||||
import dateutil.parser
|
||||
@@ -11,14 +13,14 @@ from . import (
|
||||
CalendarSync
|
||||
)
|
||||
|
||||
def load_config():
|
||||
|
||||
def load_config() -> Dict[str, Any]:
|
||||
with open('config.yml', 'r', encoding='utf-8') as f:
|
||||
result = yaml.safe_load(f)
|
||||
return result
|
||||
|
||||
|
||||
def get_start_date(date_str):
|
||||
result = datetime.datetime(1,1,1)
|
||||
def get_start_date(date_str: str) -> datetime.datetime:
|
||||
if 'now' == date_str:
|
||||
result = datetime.datetime.utcnow()
|
||||
else:
|
||||
@@ -32,8 +34,8 @@ def main():
|
||||
if 'logging' in config:
|
||||
logging.config.dictConfig(config['logging'])
|
||||
|
||||
calendarId = config['calendar']['google_id']
|
||||
ics_filepath = config['calendar']['source']
|
||||
calendarId: str = config['calendar']['google_id']
|
||||
ics_filepath: str = config['calendar']['source']
|
||||
|
||||
start = get_start_date(config['start_from'])
|
||||
|
||||
@@ -47,5 +49,6 @@ def main():
|
||||
sync.prepare_sync(start)
|
||||
sync.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
105
tests/bench_converter.py
Normal file
105
tests/bench_converter.py
Normal 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()
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from sync_ics2gcal import CalendarConverter
|
||||
@@ -26,11 +28,11 @@ LAST-MODIFIED:20180326T120235Z
|
||||
"""
|
||||
|
||||
|
||||
def ics_test_cal(content):
|
||||
def ics_test_cal(content: str) -> str:
|
||||
return "BEGIN:VCALENDAR\r\n{}END:VCALENDAR\r\n".format(content)
|
||||
|
||||
|
||||
def ics_test_event(content):
|
||||
def ics_test_event(content: str) -> str:
|
||||
return ics_test_cal("BEGIN:VEVENT\r\n{}END:VEVENT\r\n".format(content))
|
||||
|
||||
|
||||
@@ -68,7 +70,7 @@ def param_events_start_end(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_event_start_end(param_events_start_end):
|
||||
def test_event_start_end(param_events_start_end: Tuple[str, str, str, str]):
|
||||
(date_type, ics_str, start, end) = param_events_start_end
|
||||
converter = CalendarConverter()
|
||||
converter.loads(ics_str)
|
||||
|
||||
@@ -3,6 +3,7 @@ import hashlib
|
||||
import operator
|
||||
from copy import deepcopy
|
||||
from random import shuffle
|
||||
from typing import Union, List, Dict, Optional
|
||||
|
||||
import dateutil.parser
|
||||
import pytest
|
||||
@@ -11,7 +12,7 @@ from pytz import timezone, utc
|
||||
from sync_ics2gcal import CalendarSync
|
||||
|
||||
|
||||
def sha1(string):
|
||||
def sha1(string: Union[str, bytes]) -> str:
|
||||
if isinstance(string, str):
|
||||
string = string.encode('utf8')
|
||||
h = hashlib.sha1()
|
||||
@@ -19,55 +20,57 @@ def sha1(string):
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def gen_events(start, stop, start_time, no_time=False):
|
||||
def gen_events(start: int,
|
||||
stop: int,
|
||||
start_time: Union[datetime.datetime, datetime.date],
|
||||
no_time: bool = False) -> List[Dict[str, Union[str, Dict[str, str]]]]:
|
||||
if no_time:
|
||||
start_time = datetime.date(
|
||||
start_time.year, start_time.month, start_time.day)
|
||||
duration = datetime.date(1, 1, 2) - datetime.date(1, 1, 1)
|
||||
date_key = "date"
|
||||
suff = ''
|
||||
duration: datetime.timedelta = datetime.date(1, 1, 2) - datetime.date(1, 1, 1)
|
||||
date_key: str = "date"
|
||||
date_end: str = ''
|
||||
else:
|
||||
start_time = utc.normalize(
|
||||
start_time.astimezone(utc)).replace(tzinfo=None)
|
||||
duration = datetime.datetime(
|
||||
1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1)
|
||||
date_key = "dateTime"
|
||||
suff = 'Z'
|
||||
duration: datetime.timedelta = datetime.datetime(1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1)
|
||||
date_key: str = "dateTime"
|
||||
date_end: str = 'Z'
|
||||
|
||||
result = []
|
||||
result: List[Dict[str, Union[str, Dict[str, str]]]] = []
|
||||
for i in range(start, stop):
|
||||
event_start = start_time + (duration * i)
|
||||
event_end = event_start + duration
|
||||
|
||||
updated = event_start
|
||||
updated: Union[datetime.datetime, datetime.date] = event_start
|
||||
if no_time:
|
||||
updated = datetime.datetime(
|
||||
updated.year, updated.month, updated.day, 0, 0, 0, 1, tzinfo=utc)
|
||||
|
||||
event = {
|
||||
event: Dict[str, Union[str, Dict[str, str]]] = {
|
||||
'summary': 'test event __ {}'.format(i),
|
||||
'location': 'la la la {}'.format(i),
|
||||
'description': 'test TEST -- test event {}'.format(i),
|
||||
"iCalUID": "{}@test.com".format(sha1("test - event {}".format(i))),
|
||||
"updated": updated.isoformat() + 'Z',
|
||||
"created": updated.isoformat() + 'Z'
|
||||
"created": updated.isoformat() + 'Z',
|
||||
'start': {date_key: event_start.isoformat() + date_end},
|
||||
'end': {date_key: event_end.isoformat() + date_end}
|
||||
}
|
||||
event['start'] = {date_key: event_start.isoformat() + suff}
|
||||
event['end'] = {date_key: event_end.isoformat() + suff}
|
||||
result.append(event)
|
||||
return result
|
||||
|
||||
|
||||
def gen_list_to_compare(start, stop):
|
||||
result = []
|
||||
def gen_list_to_compare(start: int, stop: int) -> List[Dict[str, str]]:
|
||||
result: List[Dict[str, str]] = []
|
||||
for i in range(start, stop):
|
||||
result.append({'iCalUID': 'test{:06d}'.format(i)})
|
||||
return result
|
||||
|
||||
|
||||
def get_start_date(event):
|
||||
event_start = event['start']
|
||||
start_date = None
|
||||
def get_start_date(event: Dict[str, Union[str, Dict[str, str]]]) -> Union[datetime.datetime, datetime.date]:
|
||||
event_start: Dict[str, str] = event['start']
|
||||
start_date: Optional[str] = None
|
||||
is_date = False
|
||||
if 'date' in event_start:
|
||||
start_date = event_start['date']
|
||||
@@ -113,7 +116,7 @@ def test_compare():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("no_time", [True, False], ids=['date', 'dateTime'])
|
||||
def test_filter_events_by_date(no_time):
|
||||
def test_filter_events_by_date(no_time: bool):
|
||||
msk = timezone('Europe/Moscow')
|
||||
now = utc.localize(datetime.datetime.utcnow())
|
||||
msk_now = msk.normalize(now.astimezone(msk))
|
||||
|
||||
Reference in New Issue
Block a user