Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +135 -0
- README.md +95 -12
- app.py +115 -0
- app1.py +45 -0
- configs/inference_electra_base.yaml +41 -0
- configs/inference_en_electra_base.yaml +43 -0
- configs/inference_en_electra_large.yaml +43 -0
- configs/train_distilbert.yaml +54 -0
- configs/train_en_electra_base_finetune.yaml +54 -0
- configs/train_en_electra_base_transfer.yaml +53 -0
- configs/train_en_electra_large_finetune.yaml +53 -0
- configs/train_en_electra_large_transfer.yaml +54 -0
- configs/train_roberta_base_finetune.yaml +53 -0
- configs/train_roberta_base_transfer.yaml +53 -0
- configs/train_roberta_large_finetune.yaml +53 -0
- configs/train_roberta_large_transfer.yaml +53 -0
- evaluate_squad_v2.py +136 -0
- evaluation_scriptv2.0.py +276 -0
- google-calendar-simple-api/.github/CODE_OF_CONDUCT.md +128 -0
- google-calendar-simple-api/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- google-calendar-simple-api/.github/workflows/code-cov.yml +31 -0
- google-calendar-simple-api/.github/workflows/tests.yml +31 -0
- google-calendar-simple-api/.gitignore +18 -0
- google-calendar-simple-api/.readthedocs.yml +16 -0
- google-calendar-simple-api/CONTRIBUTING.md +32 -0
- google-calendar-simple-api/LICENSE +21 -0
- google-calendar-simple-api/README.rst +92 -0
- google-calendar-simple-api/build/lib/gcsa/__init__.py +0 -0
- google-calendar-simple-api/build/lib/gcsa/_resource.py +8 -0
- google-calendar-simple-api/build/lib/gcsa/_services/__init__.py +0 -0
- google-calendar-simple-api/build/lib/gcsa/_services/acl_service.py +143 -0
- google-calendar-simple-api/build/lib/gcsa/_services/authentication.py +144 -0
- google-calendar-simple-api/build/lib/gcsa/_services/base_service.py +61 -0
- google-calendar-simple-api/build/lib/gcsa/_services/calendar_lists_service.py +123 -0
- google-calendar-simple-api/build/lib/gcsa/_services/calendars_service.py +102 -0
- google-calendar-simple-api/build/lib/gcsa/_services/colors_service.py +15 -0
- google-calendar-simple-api/build/lib/gcsa/_services/events_service.py +427 -0
- google-calendar-simple-api/build/lib/gcsa/_services/free_busy_service.py +87 -0
- google-calendar-simple-api/build/lib/gcsa/_services/settings_service.py +13 -0
- google-calendar-simple-api/build/lib/gcsa/acl.py +70 -0
- google-calendar-simple-api/build/lib/gcsa/attachment.py +72 -0
- google-calendar-simple-api/build/lib/gcsa/attendee.py +79 -0
- google-calendar-simple-api/build/lib/gcsa/calendar.py +267 -0
- google-calendar-simple-api/build/lib/gcsa/conference.py +416 -0
- google-calendar-simple-api/build/lib/gcsa/event.py +330 -0
- google-calendar-simple-api/build/lib/gcsa/free_busy.py +98 -0
- google-calendar-simple-api/build/lib/gcsa/google_calendar.py +81 -0
- google-calendar-simple-api/build/lib/gcsa/person.py +41 -0
- google-calendar-simple-api/build/lib/gcsa/recurrence.py +570 -0
- google-calendar-simple-api/build/lib/gcsa/reminders.py +137 -0
.gitignore
ADDED
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
pip-wheel-metadata/
|
24 |
+
share/python-wheels/
|
25 |
+
*.egg-info/
|
26 |
+
.installed.cfg
|
27 |
+
*.egg
|
28 |
+
MANIFEST
|
29 |
+
|
30 |
+
# PyInstaller
|
31 |
+
# Usually these files are written by a python script from a template
|
32 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
33 |
+
*.manifest
|
34 |
+
*.spec
|
35 |
+
|
36 |
+
# Installer logs
|
37 |
+
pip-log.txt
|
38 |
+
pip-delete-this-directory.txt
|
39 |
+
|
40 |
+
# Unit test / coverage reports
|
41 |
+
htmlcov/
|
42 |
+
.tox/
|
43 |
+
.nox/
|
44 |
+
.coverage
|
45 |
+
.coverage.*
|
46 |
+
.cache
|
47 |
+
nosetests.xml
|
48 |
+
coverage.xml
|
49 |
+
*.cover
|
50 |
+
*.py,cover
|
51 |
+
.hypothesis/
|
52 |
+
.pytest_cache/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
target/
|
76 |
+
|
77 |
+
# Jupyter Notebook
|
78 |
+
.ipynb_checkpoints
|
79 |
+
|
80 |
+
# IPython
|
81 |
+
profile_default/
|
82 |
+
ipython_config.py
|
83 |
+
|
84 |
+
# pyenv
|
85 |
+
.python-version
|
86 |
+
|
87 |
+
# pipenv
|
88 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
89 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
90 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
91 |
+
# install all needed dependencies.
|
92 |
+
#Pipfile.lock
|
93 |
+
|
94 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
95 |
+
__pypackages__/
|
96 |
+
|
97 |
+
# Celery stuff
|
98 |
+
celerybeat-schedule
|
99 |
+
celerybeat.pid
|
100 |
+
|
101 |
+
# SageMath parsed files
|
102 |
+
*.sage.py
|
103 |
+
|
104 |
+
# Environments
|
105 |
+
.env
|
106 |
+
.venv
|
107 |
+
env/
|
108 |
+
venv/
|
109 |
+
ENV/
|
110 |
+
env.bak/
|
111 |
+
venv.bak/
|
112 |
+
|
113 |
+
# Spyder project settings
|
114 |
+
.spyderproject
|
115 |
+
.spyproject
|
116 |
+
|
117 |
+
# Rope project settings
|
118 |
+
.ropeproject
|
119 |
+
|
120 |
+
# mkdocs documentation
|
121 |
+
/site
|
122 |
+
|
123 |
+
# mypy
|
124 |
+
.mypy_cache/
|
125 |
+
.dmypy.json
|
126 |
+
dmypy.json
|
127 |
+
|
128 |
+
# Pyre type checker
|
129 |
+
.pyre/
|
130 |
+
|
131 |
+
# Test notebook
|
132 |
+
test.ipynb
|
133 |
+
|
134 |
+
# Wandb
|
135 |
+
wandb/
|
README.md
CHANGED
@@ -1,12 +1,95 @@
|
|
1 |
-
---
|
2 |
-
title: HTK
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: HTK
|
3 |
+
app_file: app1.py
|
4 |
+
sdk: gradio
|
5 |
+
sdk_version: 4.36.1
|
6 |
+
---
|
7 |
+
# MRC-RetroReader
|
8 |
+
|
9 |
+
## Introduction
|
10 |
+
|
11 |
+
MRC-RetroReader is a machine reading comprehension (MRC) model designed for reading comprehension tasks. The model leverages advanced neural network architectures to provide high accuracy in understanding and responding to textual queries.
|
12 |
+
|
13 |
+
## Table of Contents
|
14 |
+
|
15 |
+
- [Introduction](#introduction)
|
16 |
+
- [Table of Contents](#table-of-contents)
|
17 |
+
- [Installation](#installation)
|
18 |
+
- [Usage](#usage)
|
19 |
+
- [Features](#features)
|
20 |
+
- [Dependencies](#dependencies)
|
21 |
+
- [Configuration](#configuration)
|
22 |
+
- [Documentation](#documentation)
|
23 |
+
- [Examples](#examples)
|
24 |
+
- [Troubleshooting](#troubleshooting)
|
25 |
+
- [Contributors](#contributors)
|
26 |
+
- [License](#license)
|
27 |
+
|
28 |
+
## Installation
|
29 |
+
|
30 |
+
1. Clone the repository:
|
31 |
+
```
|
32 |
+
git clone https://github.com/phanhoang1803/MRC-RetroReader.git
|
33 |
+
cd MRC-RetroReader
|
34 |
+
```
|
35 |
+
2. Install the required dependencies:
|
36 |
+
```
|
37 |
+
pip install -r requirements.txt
|
38 |
+
```
|
39 |
+
|
40 |
+
## Usage
|
41 |
+
|
42 |
+
- For notebooks: to running automatically, turn off wandb, warning if necessary:
|
43 |
+
```
|
44 |
+
wandb off
|
45 |
+
import warnings
|
46 |
+
warnings.filterwarnings('ignore')
|
47 |
+
```
|
48 |
+
- To train the model using the SQuAD v2 dataset:
|
49 |
+
```
|
50 |
+
python train_squad_v2.py --config path-to-yaml-file --module intensive --batch_size batch_size
|
51 |
+
```
|
52 |
+
|
53 |
+
## Features
|
54 |
+
|
55 |
+
- High accuracy MRC model
|
56 |
+
- Easy to train on custom datasets
|
57 |
+
- Configurable parameters for model tuning
|
58 |
+
|
59 |
+
## Dependencies
|
60 |
+
|
61 |
+
- Python 3.x
|
62 |
+
- PyTorch
|
63 |
+
- Transformers
|
64 |
+
- Tokenizers
|
65 |
+
|
66 |
+
For a full list of dependencies, see `requirements.txt`.
|
67 |
+
|
68 |
+
## Configuration
|
69 |
+
|
70 |
+
Configuration files can be found in the `configs` directory. Adjust the parameters in these files to customize the model training and evaluation.
|
71 |
+
|
72 |
+
## Documentation
|
73 |
+
|
74 |
+
For detailed documentation, refer to the `documentation` directory. This includes:
|
75 |
+
- Model architecture
|
76 |
+
- Training procedures
|
77 |
+
- Evaluation metrics
|
78 |
+
|
79 |
+
## Examples
|
80 |
+
|
81 |
+
Example training and evaluation scripts are provided in the repository. To train on the SQuAD v2 dataset:
|
82 |
+
|
83 |
+
|
84 |
+
## Troubleshooting
|
85 |
+
|
86 |
+
For common issues and their solutions, refer to the `troubleshooting guide`.
|
87 |
+
|
88 |
+
## Contributors
|
89 |
+
|
90 |
+
- phanhoang1803
|
91 |
+
|
92 |
+
## License
|
93 |
+
|
94 |
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
95 |
+
|
app.py
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import io
|
3 |
+
import os
|
4 |
+
import yaml
|
5 |
+
import pyarrow
|
6 |
+
import tokenizers
|
7 |
+
|
8 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "true"
|
9 |
+
|
10 |
+
# Setting page config to wide mode
|
11 |
+
st.set_page_config(layout="wide")
|
12 |
+
|
13 |
+
@st.cache_resource
|
14 |
+
def from_library():
|
15 |
+
from retro_reader import RetroReader
|
16 |
+
from retro_reader import constants as C
|
17 |
+
return C, RetroReader
|
18 |
+
|
19 |
+
C, RetroReader = from_library()
|
20 |
+
|
21 |
+
my_hash_func = {
|
22 |
+
io.TextIOWrapper: lambda _: None,
|
23 |
+
pyarrow.lib.Buffer: lambda _: 0,
|
24 |
+
tokenizers.Tokenizer: lambda _: None,
|
25 |
+
tokenizers.AddedToken: lambda _: None
|
26 |
+
}
|
27 |
+
|
28 |
+
@st.cache_resource(hash_funcs=my_hash_func)
|
29 |
+
def load_en_electra_base_model():
|
30 |
+
config_file = "configs/inference_en_electra_base.yaml"
|
31 |
+
return RetroReader.load(config_file=config_file)
|
32 |
+
|
33 |
+
@st.cache_resource(hash_funcs=my_hash_func)
|
34 |
+
def load_en_electra_large_model():
|
35 |
+
config_file = "configs/inference_en_electra_large.yaml"
|
36 |
+
return RetroReader.load(config_file=config_file)
|
37 |
+
|
38 |
+
RETRO_READER_HOST = {
|
39 |
+
"google/electra-base-discriminator": load_en_electra_base_model(),
|
40 |
+
"google/electra-large-discriminator": load_en_electra_large_model(),
|
41 |
+
}
|
42 |
+
|
43 |
+
def display_top_predictions(nbest_preds, top_k=10):
|
44 |
+
# Assuming nbest_preds might be a dictionary with a key that contains the list
|
45 |
+
if not isinstance(nbest_preds, list):
|
46 |
+
nbest_preds = nbest_preds['id-01'] # Adjust key as per actual structure
|
47 |
+
|
48 |
+
sorted_preds = sorted(nbest_preds, key=lambda x: x['probability'], reverse=True)[:top_k]
|
49 |
+
st.markdown("### Top Predictions")
|
50 |
+
for i, pred in enumerate(sorted_preds, 1):
|
51 |
+
st.markdown(f"**{i}. {pred['text']}** - Probability: {pred['probability']*100:.2f}%")
|
52 |
+
|
53 |
+
def main():
|
54 |
+
# Sidebar Introduction
|
55 |
+
st.sidebar.title("📝 Welcome to Retro Reader")
|
56 |
+
st.sidebar.write("""
|
57 |
+
MRC-RetroReader is a machine reading comprehension (MRC) model designed for reading comprehension tasks. The model leverages advanced neural network architectures to provide high accuracy in understanding and responding to textual queries.
|
58 |
+
""")
|
59 |
+
image_url = "img.jpg" # Replace this URL with your actual image URL or local path
|
60 |
+
st.sidebar.image(image_url, use_column_width=True)
|
61 |
+
st.sidebar.title("Contributors")
|
62 |
+
st.sidebar.write("""
|
63 |
+
- Phan Van Hoang
|
64 |
+
- Pham Long Khanh
|
65 |
+
""")
|
66 |
+
|
67 |
+
st.title("Retrospective Reader Demo")
|
68 |
+
st.markdown("## Model name🚨")
|
69 |
+
option = st.selectbox(
|
70 |
+
label="Choose the model used in retro reader",
|
71 |
+
options=(
|
72 |
+
"[1] google/electra-base-discriminator",
|
73 |
+
"[2] google/electra-large-discriminator"
|
74 |
+
),
|
75 |
+
index=1,
|
76 |
+
)
|
77 |
+
lang_code, model_name = option.split(" ")
|
78 |
+
retro_reader = RETRO_READER_HOST[model_name]
|
79 |
+
|
80 |
+
lang_prefix = "EN"
|
81 |
+
height = 200
|
82 |
+
return_submodule_outputs = True
|
83 |
+
|
84 |
+
with st.form(key="my_form"):
|
85 |
+
st.markdown("## Type your query ❓")
|
86 |
+
query = st.text_input(
|
87 |
+
label="",
|
88 |
+
value=getattr(C, f"{lang_prefix}_EXAMPLE_QUERY"),
|
89 |
+
max_chars=None,
|
90 |
+
help=getattr(C, f"{lang_prefix}_QUERY_HELP_TEXT"),
|
91 |
+
)
|
92 |
+
st.markdown("## Type your query 💬")
|
93 |
+
context = st.text_area(
|
94 |
+
label="",
|
95 |
+
value=getattr(C, f"{lang_prefix}_EXAMPLE_CONTEXTS"),
|
96 |
+
height=height,
|
97 |
+
max_chars=None,
|
98 |
+
help=getattr(C, f"{lang_prefix}_CONTEXT_HELP_TEXT"),
|
99 |
+
)
|
100 |
+
submit_button = st.form_submit_button(label="Submit")
|
101 |
+
|
102 |
+
if submit_button:
|
103 |
+
with st.spinner("🕒 Please wait.."):
|
104 |
+
outputs = retro_reader(query=query, context=context, return_submodule_outputs=return_submodule_outputs)
|
105 |
+
answer, score = outputs[0]["id-01"], outputs[1]
|
106 |
+
if not answer:
|
107 |
+
answer = "No answer"
|
108 |
+
st.markdown("## 📜 Results")
|
109 |
+
st.write(answer)
|
110 |
+
if return_submodule_outputs:
|
111 |
+
score_ext, nbest_preds, score_diff = outputs[2:]
|
112 |
+
display_top_predictions(nbest_preds)
|
113 |
+
|
114 |
+
if __name__ == "__main__":
|
115 |
+
main()
|
app1.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import io
|
3 |
+
import os
|
4 |
+
import yaml
|
5 |
+
import pyarrow
|
6 |
+
import tokenizers
|
7 |
+
from retro_reader import RetroReader
|
8 |
+
|
9 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "true"
|
10 |
+
|
11 |
+
def from_library():
|
12 |
+
from retro_reader import constants as C
|
13 |
+
return C, RetroReader
|
14 |
+
|
15 |
+
C, RetroReader = from_library()
|
16 |
+
|
17 |
+
# Assuming RetroReader.load is a method from your imports
|
18 |
+
def load_model(config_path):
|
19 |
+
return RetroReader.load(config_file=config_path)
|
20 |
+
|
21 |
+
# Loading models
|
22 |
+
model_base = load_model("configs/inference_en_electra_base.yaml")
|
23 |
+
model_large = load_model("configs/inference_en_electra_large.yaml")
|
24 |
+
|
25 |
+
def retro_reader_demo(query, context, model_choice):
|
26 |
+
model = model_base if model_choice == "Base" else model_large
|
27 |
+
outputs = model(query=query, context=context, return_submodule_outputs=True)
|
28 |
+
answer = outputs[0]["id-01"] if outputs[0]["id-01"] else "No answer found"
|
29 |
+
return answer
|
30 |
+
|
31 |
+
# Gradio app interface
|
32 |
+
iface = gr.Interface(
|
33 |
+
fn=retro_reader_demo,
|
34 |
+
inputs=[
|
35 |
+
gr.Textbox(label="Query", placeholder="Type your query here..."),
|
36 |
+
gr.Textbox(label="Context", placeholder="Provide the context here...", lines=10),
|
37 |
+
gr.Radio(choices=["Base", "Large"], label="Model Choice")
|
38 |
+
],
|
39 |
+
outputs=gr.Textbox(label="Answer"),
|
40 |
+
title="Retrospective Reader Demo",
|
41 |
+
description="This interface uses the RetroReader model to perform reading comprehension tasks."
|
42 |
+
)
|
43 |
+
|
44 |
+
if __name__ == "__main__":
|
45 |
+
iface.launch(share=True)
|
configs/inference_electra_base.yaml
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# ModelArguments
|
23 |
+
use_auth_token: False
|
24 |
+
|
25 |
+
# SketchModelArguments
|
26 |
+
sketch_revision: en-electra-large-sketch
|
27 |
+
sketch_model_name: jinmang2/retro-reader
|
28 |
+
sketch_architectures: ElectraForSequenceClassification
|
29 |
+
|
30 |
+
# IntensiveModelArguments
|
31 |
+
intensive_revision: en-electra-large-intensive
|
32 |
+
intensive_model_name: jinmang2/retro-reader
|
33 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
34 |
+
|
35 |
+
|
36 |
+
TrainingArguments:
|
37 |
+
output_dir: outputs
|
38 |
+
no_cuda: True # If you want to use cuda,
|
39 |
+
# change `no_cuda` to False and `fp16` to True
|
40 |
+
per_device_train_batch_size: 1
|
41 |
+
per_device_eval_batch_size: 12
|
configs/inference_en_electra_base.yaml
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# ModelArguments
|
23 |
+
use_auth_token: False
|
24 |
+
|
25 |
+
# SketchModelArguments
|
26 |
+
sketch_revision: en-electra-base-sketch
|
27 |
+
sketch_model_name: faori/retro_reeader
|
28 |
+
# sketch_model_mode: transfer
|
29 |
+
sketch_architectures: ElectraForSequenceClassification
|
30 |
+
|
31 |
+
# IntensiveModelArguments
|
32 |
+
intensive_revision: en-electra-base-intensive
|
33 |
+
intensive_model_name: faori/retro_reeader
|
34 |
+
# intensive_model_mode: transfer
|
35 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
36 |
+
|
37 |
+
|
38 |
+
TrainingArguments:
|
39 |
+
output_dir: outputs
|
40 |
+
no_cuda: True # If you want to use cuda,
|
41 |
+
# change `no_cuda` to False and `fp16` to True
|
42 |
+
per_device_train_batch_size: 1
|
43 |
+
per_device_eval_batch_size: 12
|
configs/inference_en_electra_large.yaml
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# ModelArguments
|
23 |
+
use_auth_token: False
|
24 |
+
|
25 |
+
# SketchModelArguments
|
26 |
+
sketch_revision: en-electra-large-sketch
|
27 |
+
sketch_model_name: jinmang2/retro-reader
|
28 |
+
# sketch_model_mode: transfer
|
29 |
+
sketch_architectures: ElectraForSequenceClassification
|
30 |
+
|
31 |
+
# IntensiveModelArguments
|
32 |
+
intensive_revision: en-electra-large-intensive
|
33 |
+
intensive_model_name: jinmang2/retro-reader
|
34 |
+
# intensive_model_mode: transfer
|
35 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
36 |
+
|
37 |
+
|
38 |
+
TrainingArguments:
|
39 |
+
output_dir: outputs
|
40 |
+
no_cuda: True # If you want to use cuda,
|
41 |
+
# change `no_cuda` to False and `fp16` to True
|
42 |
+
per_device_train_batch_size: 1
|
43 |
+
per_device_eval_batch_size: 12
|
configs/train_distilbert.yaml
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: distilbert/distilbert-base-uncased
|
24 |
+
# sketch_model_mode: transfer
|
25 |
+
sketch_architectures: DistilBertForSequenceClassification
|
26 |
+
|
27 |
+
# IntensiveModelArguments
|
28 |
+
intensive_model_name: distilbert-base-uncased
|
29 |
+
intensive_model_mode: transfer
|
30 |
+
intensive_architectures: DistilBertForQuestionAnsweringAVPool
|
31 |
+
|
32 |
+
|
33 |
+
TrainingArguments:
|
34 |
+
# report_to: wandb
|
35 |
+
run_name: squadv2-distilbert-base-sketch,squadv2-distilbert-base-intensive
|
36 |
+
output_dir: outputs
|
37 |
+
overwrite_output_dir: False
|
38 |
+
learning_rate: 2e-5
|
39 |
+
evaluation_strategy: epoch
|
40 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
41 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
42 |
+
save_steps: 5000
|
43 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
44 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
45 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
46 |
+
# no need to specify logging_dir, it defaults to output_dir
|
47 |
+
per_device_train_batch_size: 64
|
48 |
+
per_device_eval_batch_size: 64
|
49 |
+
num_train_epochs: 10.0
|
50 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
51 |
+
no_cuda: False
|
52 |
+
fp16: True
|
53 |
+
warmup_ratio: 0.1
|
54 |
+
weight_decay: 0.01
|
configs/train_en_electra_base_finetune.yaml
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: google/electra-base-discriminator
|
24 |
+
sketch_architectures: ElectraForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: google/electra-base-discriminator
|
28 |
+
intensive_model_mode: finetune
|
29 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-electra-base-sketch,squadv2-electra-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 64
|
47 |
+
per_device_eval_batch_size: 64
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
54 |
+
|
configs/train_en_electra_base_transfer.yaml
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: google/electra-base-discriminator
|
24 |
+
sketch_architectures: ElectraForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: google/electra-base-discriminator
|
28 |
+
intensive_model_mode: transfer
|
29 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-electra-base-sketch,squadv2-electra-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 512
|
47 |
+
per_device_eval_batch_size: 512
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
configs/train_en_electra_large_finetune.yaml
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: google/electra-large-discriminator
|
24 |
+
sketch_architectures: ElectraForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: google/electra-large-discriminator
|
28 |
+
intensive_model_mode: finetune
|
29 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-electra-base-sketch,squadv2-electra-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 8
|
47 |
+
per_device_eval_batch_size: 8
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
configs/train_en_electra_large_transfer.yaml
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: google/electra-large-discriminator
|
24 |
+
# sketch_model_mode: transfer
|
25 |
+
sketch_architectures: ElectraForSequenceClassification
|
26 |
+
|
27 |
+
# IntensiveModelArguments
|
28 |
+
intensive_model_name: google/electra-large-discriminator
|
29 |
+
intensive_model_mode: transfer
|
30 |
+
intensive_architectures: ElectraForQuestionAnsweringAVPool
|
31 |
+
|
32 |
+
|
33 |
+
TrainingArguments:
|
34 |
+
# report_to: wandb
|
35 |
+
run_name: squadv2-electra-base-sketch,squadv2-electra-base-intensive
|
36 |
+
output_dir: outputs
|
37 |
+
overwrite_output_dir: False
|
38 |
+
learning_rate: 2e-5
|
39 |
+
evaluation_strategy: epoch
|
40 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
41 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
42 |
+
save_steps: 5000
|
43 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
44 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
45 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
46 |
+
# no need to specify logging_dir, it defaults to output_dir
|
47 |
+
per_device_train_batch_size: 512
|
48 |
+
per_device_eval_batch_size: 512
|
49 |
+
num_train_epochs: 10.0
|
50 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
51 |
+
no_cuda: False
|
52 |
+
fp16: True
|
53 |
+
warmup_ratio: 0.1
|
54 |
+
weight_decay: 0.01
|
configs/train_roberta_base_finetune.yaml
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: FacebookAI/roberta-base
|
24 |
+
sketch_architectures: RobertaForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: FacebookAI/roberta-base
|
28 |
+
intensive_model_mode: finetune
|
29 |
+
intensive_architectures: RobertaForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-roberta-base-sketch,squadv2-roberta-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 64
|
47 |
+
per_device_eval_batch_size: 64
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
configs/train_roberta_base_transfer.yaml
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: FacebookAI/roberta-base
|
24 |
+
sketch_architectures: RobertaForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: FacebookAI/roberta-base
|
28 |
+
intensive_model_mode: transfer
|
29 |
+
intensive_architectures: RobertaForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-roberta-base-sketch,squadv2-roberta-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 512
|
47 |
+
per_device_eval_batch_size: 512
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
configs/train_roberta_large_finetune.yaml
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: FacebookAI/roberta-large
|
24 |
+
sketch_architectures: RobertaForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: FacebookAI/roberta-large
|
28 |
+
intensive_model_mode: finetune
|
29 |
+
intensive_architectures: RobertaForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-roberta-base-sketch,squadv2-roberta-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 512
|
47 |
+
per_device_eval_batch_size: 512
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
configs/train_roberta_large_transfer.yaml
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RetroDataModelArguments:
|
2 |
+
|
3 |
+
# DataArguments
|
4 |
+
max_seq_length: 512
|
5 |
+
max_answer_length: 30
|
6 |
+
doc_stride: 128
|
7 |
+
return_token_type_ids: True
|
8 |
+
pad_to_max_length: True
|
9 |
+
preprocessing_num_workers: 5
|
10 |
+
overwrite_cache: False
|
11 |
+
version_2_with_negative: True
|
12 |
+
null_score_diff_threshold: 0.0
|
13 |
+
rear_threshold: 0.0
|
14 |
+
n_best_size: 20
|
15 |
+
use_choice_logits: False
|
16 |
+
start_n_top: -1
|
17 |
+
end_n_top: -1
|
18 |
+
beta1: 1
|
19 |
+
beta2: 1
|
20 |
+
best_cof: 1
|
21 |
+
|
22 |
+
# SketchModelArguments
|
23 |
+
sketch_model_name: FacebookAI/roberta-large
|
24 |
+
sketch_architectures: RobertaForSequenceClassification
|
25 |
+
|
26 |
+
# IntensiveModelArguments
|
27 |
+
intensive_model_name: FacebookAI/roberta-large
|
28 |
+
intensive_model_mode: transfer
|
29 |
+
intensive_architectures: RobertaForQuestionAnsweringAVPool
|
30 |
+
|
31 |
+
|
32 |
+
TrainingArguments:
|
33 |
+
# report_to: wandb
|
34 |
+
run_name: squadv2-roberta-base-sketch,squadv2-roberta-base-intensive
|
35 |
+
output_dir: outputs
|
36 |
+
overwrite_output_dir: False
|
37 |
+
learning_rate: 2e-5
|
38 |
+
evaluation_strategy: epoch
|
39 |
+
save_strategy: steps # Save checkpoints every specified number of steps
|
40 |
+
# save_steps: 5000 # Save model checkpoints every 5000 steps
|
41 |
+
save_steps: 5000
|
42 |
+
save_total_limit: 2 # Maximum number of checkpoints to keep
|
43 |
+
# load_best_model_at_end: True # Disable to avoid loading the best model at the end
|
44 |
+
# no need to specify checkpoint_dir, it defaults to output_dir
|
45 |
+
# no need to specify logging_dir, it defaults to output_dir
|
46 |
+
per_device_train_batch_size: 512
|
47 |
+
per_device_eval_batch_size: 512
|
48 |
+
num_train_epochs: 10.0
|
49 |
+
# no need to specify metric_for_best_model for resuming from checkpoints
|
50 |
+
no_cuda: False
|
51 |
+
fp16: True
|
52 |
+
warmup_ratio: 0.1
|
53 |
+
weight_decay: 0.01
|
evaluate_squad_v2.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
os.environ["TF_ENABLE_ONEDNN_OPTS"] = '0'
|
3 |
+
|
4 |
+
from huggingface_hub import login
|
5 |
+
|
6 |
+
|
7 |
+
from typing import Union, Any, Dict
|
8 |
+
# from datasets.arrow_dataset import Batch
|
9 |
+
|
10 |
+
import argparse
|
11 |
+
import datasets
|
12 |
+
from transformers.utils import logging, check_min_version
|
13 |
+
from transformers.utils.versions import require_version
|
14 |
+
|
15 |
+
from retro_reader import RetroReader
|
16 |
+
from retro_reader.constants import EXAMPLE_FEATURES
|
17 |
+
import torch
|
18 |
+
|
19 |
+
# Will error if the minimal version of Transformers is not installed. Remove at your own risks.
|
20 |
+
check_min_version("4.13.0.dev0")
|
21 |
+
|
22 |
+
require_version("datasets>=1.8.0")
|
23 |
+
|
24 |
+
logger = logging.get_logger(__name__)
|
25 |
+
|
26 |
+
|
27 |
+
def schema_integrate(example) -> Union[Dict, Any]:
|
28 |
+
title = example["title"]
|
29 |
+
question = example["question"]
|
30 |
+
context = example["context"]
|
31 |
+
guid = example["id"]
|
32 |
+
classtype = [""] * len(title)
|
33 |
+
dataset_name = source = ["squad_v2"] * len(title)
|
34 |
+
answers, is_impossible = [], []
|
35 |
+
for answer_examples in example["answers"]:
|
36 |
+
if answer_examples["text"]:
|
37 |
+
answers.append(answer_examples)
|
38 |
+
is_impossible.append(False)
|
39 |
+
else:
|
40 |
+
answers.append({"text": [""], "answer_start": [-1]})
|
41 |
+
is_impossible.append(True)
|
42 |
+
# The feature names must be sorted.
|
43 |
+
return {
|
44 |
+
"guid": guid,
|
45 |
+
"question": question,
|
46 |
+
"context": context,
|
47 |
+
"answers": answers,
|
48 |
+
"title": title,
|
49 |
+
"classtype": classtype,
|
50 |
+
"source": source,
|
51 |
+
"is_impossible": is_impossible,
|
52 |
+
"dataset": dataset_name,
|
53 |
+
}
|
54 |
+
|
55 |
+
|
56 |
+
# data augmentation for multiple answers
|
57 |
+
def data_aug_for_multiple_answers(examples) -> Union[Dict, Any]:
|
58 |
+
result = {key: [] for key in examples.keys()}
|
59 |
+
|
60 |
+
def update(i, answers=None):
|
61 |
+
for key in result.keys():
|
62 |
+
if key == "answers" and answers is not None:
|
63 |
+
result[key].append(answers)
|
64 |
+
else:
|
65 |
+
result[key].append(examples[key][i])
|
66 |
+
|
67 |
+
for i, (answers, unanswerable) in enumerate(
|
68 |
+
zip(examples["answers"], examples["is_impossible"])
|
69 |
+
):
|
70 |
+
answerable = not unanswerable
|
71 |
+
assert (
|
72 |
+
len(answers["text"]) == len(answers["answer_start"]) or
|
73 |
+
answers["answer_start"][0] == -1
|
74 |
+
)
|
75 |
+
if answerable and len(answers["text"]) > 1:
|
76 |
+
for n_ans in range(len(answers["text"])):
|
77 |
+
ans = {
|
78 |
+
"text": [answers["text"][n_ans]],
|
79 |
+
"answer_start": [answers["answer_start"][n_ans]],
|
80 |
+
}
|
81 |
+
update(i, ans)
|
82 |
+
elif not answerable:
|
83 |
+
update(i, {"text": [], "answer_start": []})
|
84 |
+
else:
|
85 |
+
update(i)
|
86 |
+
|
87 |
+
return result
|
88 |
+
|
89 |
+
|
90 |
+
def main(args):
|
91 |
+
# Load SQuAD V2.0 dataset
|
92 |
+
print("Loading SQuAD v2.0 dataset ...")
|
93 |
+
squad_v2 = datasets.load_dataset("squad_v2")
|
94 |
+
|
95 |
+
# TODO: Visualize a sample from the dataset
|
96 |
+
|
97 |
+
# Integrate into the schema used in this library
|
98 |
+
# Note: The columns used for preprocessing are `question`, `context`, `answers`
|
99 |
+
# and `is_impossible`. The remaining columns are columns that exist to
|
100 |
+
# process other types of data.
|
101 |
+
|
102 |
+
# Minize the dataset for debugging
|
103 |
+
if args.debug:
|
104 |
+
squad_v2["validation"] = squad_v2["validation"].select(range(5))
|
105 |
+
|
106 |
+
print("Integrating into the schema used in this library ...")
|
107 |
+
squad_v2 = squad_v2.map(
|
108 |
+
schema_integrate,
|
109 |
+
batched=True,
|
110 |
+
remove_columns=squad_v2.column_names["train"],
|
111 |
+
features=EXAMPLE_FEATURES,
|
112 |
+
)
|
113 |
+
# Load Retro Reader
|
114 |
+
# features: parse arguments
|
115 |
+
# make train/eval dataset from examples
|
116 |
+
# load model from 🤗 hub
|
117 |
+
# set sketch/intensive reader and rear verifier
|
118 |
+
print("Loading Retro Reader ...")
|
119 |
+
retro_reader = RetroReader.load(
|
120 |
+
config_file=args.configs,
|
121 |
+
device="cuda" if torch.cuda.is_available() else "cpu",
|
122 |
+
)
|
123 |
+
|
124 |
+
# Train
|
125 |
+
res = retro_reader.evaluate(squad_v2["validation"])
|
126 |
+
print(res)
|
127 |
+
logger.warning("Train retrospective reader Done.")
|
128 |
+
|
129 |
+
|
130 |
+
if __name__ == "__main__":
|
131 |
+
parser = argparse.ArgumentParser()
|
132 |
+
parser.add_argument("--configs", "-c", type=str, default="configs/inference_electra_base.yaml", help="config file path")
|
133 |
+
parser.add_argument("--batch_size", "-b", type=int, default=1024, help="batch size")
|
134 |
+
parser.add_argument("--debug", "-d", action="store_true", help="debug mode")
|
135 |
+
args = parser.parse_args()
|
136 |
+
main(args)
|
evaluation_scriptv2.0.py
ADDED
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Official evaluation script for SQuAD version 2.0.
|
2 |
+
|
3 |
+
In addition to basic functionality, we also compute additional statistics and
|
4 |
+
plot precision-recall curves if an additional na_prob.json file is provided.
|
5 |
+
This file is expected to map question ID's to the model's predicted probability
|
6 |
+
that a question is unanswerable.
|
7 |
+
"""
|
8 |
+
import argparse
|
9 |
+
import collections
|
10 |
+
import json
|
11 |
+
import numpy as np
|
12 |
+
import os
|
13 |
+
import re
|
14 |
+
import string
|
15 |
+
import sys
|
16 |
+
|
17 |
+
OPTS = None
|
18 |
+
|
19 |
+
def parse_args():
|
20 |
+
parser = argparse.ArgumentParser('Official evaluation script for SQuAD version 2.0.')
|
21 |
+
parser.add_argument('data_file', metavar='data.json', help='Input data JSON file.')
|
22 |
+
parser.add_argument('pred_file', metavar='pred.json', help='Model predictions.')
|
23 |
+
parser.add_argument('--out-file', '-o', metavar='eval.json',
|
24 |
+
help='Write accuracy metrics to file (default is stdout).')
|
25 |
+
parser.add_argument('--na-prob-file', '-n', metavar='na_prob.json',
|
26 |
+
help='Model estimates of probability of no answer.')
|
27 |
+
parser.add_argument('--na-prob-thresh', '-t', type=float, default=1.0,
|
28 |
+
help='Predict "" if no-answer probability exceeds this (default = 1.0).')
|
29 |
+
parser.add_argument('--out-image-dir', '-p', metavar='out_images', default=None,
|
30 |
+
help='Save precision-recall curves to directory.')
|
31 |
+
parser.add_argument('--verbose', '-v', action='store_true')
|
32 |
+
if len(sys.argv) == 1:
|
33 |
+
parser.print_help()
|
34 |
+
sys.exit(1)
|
35 |
+
return parser.parse_args()
|
36 |
+
|
37 |
+
def make_qid_to_has_ans(dataset):
|
38 |
+
qid_to_has_ans = {}
|
39 |
+
for article in dataset:
|
40 |
+
for p in article['paragraphs']:
|
41 |
+
for qa in p['qas']:
|
42 |
+
qid_to_has_ans[qa['id']] = bool(qa['answers'])
|
43 |
+
return qid_to_has_ans
|
44 |
+
|
45 |
+
def normalize_answer(s):
|
46 |
+
"""Lower text and remove punctuation, articles and extra whitespace."""
|
47 |
+
def remove_articles(text):
|
48 |
+
regex = re.compile(r'\b(a|an|the)\b', re.UNICODE)
|
49 |
+
return re.sub(regex, ' ', text)
|
50 |
+
def white_space_fix(text):
|
51 |
+
return ' '.join(text.split())
|
52 |
+
def remove_punc(text):
|
53 |
+
exclude = set(string.punctuation)
|
54 |
+
return ''.join(ch for ch in text if ch not in exclude)
|
55 |
+
def lower(text):
|
56 |
+
return text.lower()
|
57 |
+
return white_space_fix(remove_articles(remove_punc(lower(s))))
|
58 |
+
|
59 |
+
def get_tokens(s):
|
60 |
+
if not s: return []
|
61 |
+
return normalize_answer(s).split()
|
62 |
+
|
63 |
+
def compute_exact(a_gold, a_pred):
|
64 |
+
return int(normalize_answer(a_gold) == normalize_answer(a_pred))
|
65 |
+
|
66 |
+
def compute_f1(a_gold, a_pred):
|
67 |
+
gold_toks = get_tokens(a_gold)
|
68 |
+
pred_toks = get_tokens(a_pred)
|
69 |
+
common = collections.Counter(gold_toks) & collections.Counter(pred_toks)
|
70 |
+
num_same = sum(common.values())
|
71 |
+
if len(gold_toks) == 0 or len(pred_toks) == 0:
|
72 |
+
# If either is no-answer, then F1 is 1 if they agree, 0 otherwise
|
73 |
+
return int(gold_toks == pred_toks)
|
74 |
+
if num_same == 0:
|
75 |
+
return 0
|
76 |
+
precision = 1.0 * num_same / len(pred_toks)
|
77 |
+
recall = 1.0 * num_same / len(gold_toks)
|
78 |
+
f1 = (2 * precision * recall) / (precision + recall)
|
79 |
+
return f1
|
80 |
+
|
81 |
+
def get_raw_scores(dataset, preds):
|
82 |
+
exact_scores = {}
|
83 |
+
f1_scores = {}
|
84 |
+
for article in dataset:
|
85 |
+
for p in article['paragraphs']:
|
86 |
+
for qa in p['qas']:
|
87 |
+
qid = qa['id']
|
88 |
+
gold_answers = [a['text'] for a in qa['answers']
|
89 |
+
if normalize_answer(a['text'])]
|
90 |
+
if not gold_answers:
|
91 |
+
# For unanswerable questions, only correct answer is empty string
|
92 |
+
gold_answers = ['']
|
93 |
+
if qid not in preds:
|
94 |
+
print('Missing prediction for %s' % qid)
|
95 |
+
continue
|
96 |
+
a_pred = preds[qid]
|
97 |
+
# Take max over all gold answers
|
98 |
+
exact_scores[qid] = max(compute_exact(a, a_pred) for a in gold_answers)
|
99 |
+
f1_scores[qid] = max(compute_f1(a, a_pred) for a in gold_answers)
|
100 |
+
return exact_scores, f1_scores
|
101 |
+
|
102 |
+
def apply_no_ans_threshold(scores, na_probs, qid_to_has_ans, na_prob_thresh):
|
103 |
+
new_scores = {}
|
104 |
+
for qid, s in scores.items():
|
105 |
+
pred_na = na_probs[qid] > na_prob_thresh
|
106 |
+
if pred_na:
|
107 |
+
new_scores[qid] = float(not qid_to_has_ans[qid])
|
108 |
+
else:
|
109 |
+
new_scores[qid] = s
|
110 |
+
return new_scores
|
111 |
+
|
112 |
+
def make_eval_dict(exact_scores, f1_scores, qid_list=None):
|
113 |
+
if not qid_list:
|
114 |
+
total = len(exact_scores)
|
115 |
+
return collections.OrderedDict([
|
116 |
+
('exact', 100.0 * sum(exact_scores.values()) / total),
|
117 |
+
('f1', 100.0 * sum(f1_scores.values()) / total),
|
118 |
+
('total', total),
|
119 |
+
])
|
120 |
+
else:
|
121 |
+
total = len(qid_list)
|
122 |
+
return collections.OrderedDict([
|
123 |
+
('exact', 100.0 * sum(exact_scores[k] for k in qid_list) / total),
|
124 |
+
('f1', 100.0 * sum(f1_scores[k] for k in qid_list) / total),
|
125 |
+
('total', total),
|
126 |
+
])
|
127 |
+
|
128 |
+
def merge_eval(main_eval, new_eval, prefix):
|
129 |
+
for k in new_eval:
|
130 |
+
main_eval['%s_%s' % (prefix, k)] = new_eval[k]
|
131 |
+
|
132 |
+
def plot_pr_curve(precisions, recalls, out_image, title):
|
133 |
+
plt.step(recalls, precisions, color='b', alpha=0.2, where='post')
|
134 |
+
plt.fill_between(recalls, precisions, step='post', alpha=0.2, color='b')
|
135 |
+
plt.xlabel('Recall')
|
136 |
+
plt.ylabel('Precision')
|
137 |
+
plt.xlim([0.0, 1.05])
|
138 |
+
plt.ylim([0.0, 1.05])
|
139 |
+
plt.title(title)
|
140 |
+
plt.savefig(out_image)
|
141 |
+
plt.clf()
|
142 |
+
|
143 |
+
def make_precision_recall_eval(scores, na_probs, num_true_pos, qid_to_has_ans,
|
144 |
+
out_image=None, title=None):
|
145 |
+
qid_list = sorted(na_probs, key=lambda k: na_probs[k])
|
146 |
+
true_pos = 0.0
|
147 |
+
cur_p = 1.0
|
148 |
+
cur_r = 0.0
|
149 |
+
precisions = [1.0]
|
150 |
+
recalls = [0.0]
|
151 |
+
avg_prec = 0.0
|
152 |
+
for i, qid in enumerate(qid_list):
|
153 |
+
if qid_to_has_ans[qid]:
|
154 |
+
true_pos += scores[qid]
|
155 |
+
cur_p = true_pos / float(i+1)
|
156 |
+
cur_r = true_pos / float(num_true_pos)
|
157 |
+
if i == len(qid_list) - 1 or na_probs[qid] != na_probs[qid_list[i+1]]:
|
158 |
+
# i.e., if we can put a threshold after this point
|
159 |
+
avg_prec += cur_p * (cur_r - recalls[-1])
|
160 |
+
precisions.append(cur_p)
|
161 |
+
recalls.append(cur_r)
|
162 |
+
if out_image:
|
163 |
+
plot_pr_curve(precisions, recalls, out_image, title)
|
164 |
+
return {'ap': 100.0 * avg_prec}
|
165 |
+
|
166 |
+
def run_precision_recall_analysis(main_eval, exact_raw, f1_raw, na_probs,
|
167 |
+
qid_to_has_ans, out_image_dir):
|
168 |
+
if out_image_dir and not os.path.exists(out_image_dir):
|
169 |
+
os.makedirs(out_image_dir)
|
170 |
+
num_true_pos = sum(1 for v in qid_to_has_ans.values() if v)
|
171 |
+
if num_true_pos == 0:
|
172 |
+
return
|
173 |
+
pr_exact = make_precision_recall_eval(
|
174 |
+
exact_raw, na_probs, num_true_pos, qid_to_has_ans,
|
175 |
+
out_image=os.path.join(out_image_dir, 'pr_exact.png'),
|
176 |
+
title='Precision-Recall curve for Exact Match score')
|
177 |
+
pr_f1 = make_precision_recall_eval(
|
178 |
+
f1_raw, na_probs, num_true_pos, qid_to_has_ans,
|
179 |
+
out_image=os.path.join(out_image_dir, 'pr_f1.png'),
|
180 |
+
title='Precision-Recall curve for F1 score')
|
181 |
+
oracle_scores = {k: float(v) for k, v in qid_to_has_ans.items()}
|
182 |
+
pr_oracle = make_precision_recall_eval(
|
183 |
+
oracle_scores, na_probs, num_true_pos, qid_to_has_ans,
|
184 |
+
out_image=os.path.join(out_image_dir, 'pr_oracle.png'),
|
185 |
+
title='Oracle Precision-Recall curve (binary task of HasAns vs. NoAns)')
|
186 |
+
merge_eval(main_eval, pr_exact, 'pr_exact')
|
187 |
+
merge_eval(main_eval, pr_f1, 'pr_f1')
|
188 |
+
merge_eval(main_eval, pr_oracle, 'pr_oracle')
|
189 |
+
|
190 |
+
def histogram_na_prob(na_probs, qid_list, image_dir, name):
|
191 |
+
if not qid_list:
|
192 |
+
return
|
193 |
+
x = [na_probs[k] for k in qid_list]
|
194 |
+
weights = np.ones_like(x) / float(len(x))
|
195 |
+
plt.hist(x, weights=weights, bins=20, range=(0.0, 1.0))
|
196 |
+
plt.xlabel('Model probability of no-answer')
|
197 |
+
plt.ylabel('Proportion of dataset')
|
198 |
+
plt.title('Histogram of no-answer probability: %s' % name)
|
199 |
+
plt.savefig(os.path.join(image_dir, 'na_prob_hist_%s.png' % name))
|
200 |
+
plt.clf()
|
201 |
+
|
202 |
+
def find_best_thresh(preds, scores, na_probs, qid_to_has_ans):
|
203 |
+
num_no_ans = sum(1 for k in qid_to_has_ans if not qid_to_has_ans[k])
|
204 |
+
cur_score = num_no_ans
|
205 |
+
best_score = cur_score
|
206 |
+
best_thresh = 0.0
|
207 |
+
qid_list = sorted(na_probs, key=lambda k: na_probs[k])
|
208 |
+
for i, qid in enumerate(qid_list):
|
209 |
+
if qid not in scores: continue
|
210 |
+
if qid_to_has_ans[qid]:
|
211 |
+
diff = scores[qid]
|
212 |
+
else:
|
213 |
+
if preds[qid]:
|
214 |
+
diff = -1
|
215 |
+
else:
|
216 |
+
diff = 0
|
217 |
+
cur_score += diff
|
218 |
+
if cur_score > best_score:
|
219 |
+
best_score = cur_score
|
220 |
+
best_thresh = na_probs[qid]
|
221 |
+
return 100.0 * best_score / len(scores), best_thresh
|
222 |
+
|
223 |
+
def find_all_best_thresh(main_eval, preds, exact_raw, f1_raw, na_probs, qid_to_has_ans):
|
224 |
+
best_exact, exact_thresh = find_best_thresh(preds, exact_raw, na_probs, qid_to_has_ans)
|
225 |
+
best_f1, f1_thresh = find_best_thresh(preds, f1_raw, na_probs, qid_to_has_ans)
|
226 |
+
main_eval['best_exact'] = best_exact
|
227 |
+
main_eval['best_exact_thresh'] = exact_thresh
|
228 |
+
main_eval['best_f1'] = best_f1
|
229 |
+
main_eval['best_f1_thresh'] = f1_thresh
|
230 |
+
|
231 |
+
def main():
|
232 |
+
with open(OPTS.data_file) as f:
|
233 |
+
dataset_json = json.load(f)
|
234 |
+
dataset = dataset_json['data']
|
235 |
+
with open(OPTS.pred_file) as f:
|
236 |
+
preds = json.load(f)
|
237 |
+
if OPTS.na_prob_file:
|
238 |
+
with open(OPTS.na_prob_file) as f:
|
239 |
+
na_probs = json.load(f)
|
240 |
+
else:
|
241 |
+
na_probs = {k: 0.0 for k in preds}
|
242 |
+
qid_to_has_ans = make_qid_to_has_ans(dataset) # maps qid to True/False
|
243 |
+
has_ans_qids = [k for k, v in qid_to_has_ans.items() if v]
|
244 |
+
no_ans_qids = [k for k, v in qid_to_has_ans.items() if not v]
|
245 |
+
exact_raw, f1_raw = get_raw_scores(dataset, preds)
|
246 |
+
exact_thresh = apply_no_ans_threshold(exact_raw, na_probs, qid_to_has_ans,
|
247 |
+
OPTS.na_prob_thresh)
|
248 |
+
f1_thresh = apply_no_ans_threshold(f1_raw, na_probs, qid_to_has_ans,
|
249 |
+
OPTS.na_prob_thresh)
|
250 |
+
out_eval = make_eval_dict(exact_thresh, f1_thresh)
|
251 |
+
if has_ans_qids:
|
252 |
+
has_ans_eval = make_eval_dict(exact_thresh, f1_thresh, qid_list=has_ans_qids)
|
253 |
+
merge_eval(out_eval, has_ans_eval, 'HasAns')
|
254 |
+
if no_ans_qids:
|
255 |
+
no_ans_eval = make_eval_dict(exact_thresh, f1_thresh, qid_list=no_ans_qids)
|
256 |
+
merge_eval(out_eval, no_ans_eval, 'NoAns')
|
257 |
+
if OPTS.na_prob_file:
|
258 |
+
find_all_best_thresh(out_eval, preds, exact_raw, f1_raw, na_probs, qid_to_has_ans)
|
259 |
+
if OPTS.na_prob_file and OPTS.out_image_dir:
|
260 |
+
run_precision_recall_analysis(out_eval, exact_raw, f1_raw, na_probs,
|
261 |
+
qid_to_has_ans, OPTS.out_image_dir)
|
262 |
+
histogram_na_prob(na_probs, has_ans_qids, OPTS.out_image_dir, 'hasAns')
|
263 |
+
histogram_na_prob(na_probs, no_ans_qids, OPTS.out_image_dir, 'noAns')
|
264 |
+
if OPTS.out_file:
|
265 |
+
with open(OPTS.out_file, 'w') as f:
|
266 |
+
json.dump(out_eval, f)
|
267 |
+
else:
|
268 |
+
print(json.dumps(out_eval, indent=2))
|
269 |
+
|
270 |
+
if __name__ == '__main__':
|
271 |
+
OPTS = parse_args()
|
272 |
+
if OPTS.out_image_dir:
|
273 |
+
import matplotlib
|
274 |
+
matplotlib.use('Agg')
|
275 |
+
import matplotlib.pyplot as plt
|
276 |
+
main()
|
google-calendar-simple-api/.github/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Covenant Code of Conduct
|
2 |
+
|
3 |
+
## Our Pledge
|
4 |
+
|
5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8 |
+
identity and expression, level of experience, education, socio-economic status,
|
9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
10 |
+
and orientation.
|
11 |
+
|
12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
13 |
+
diverse, inclusive, and healthy community.
|
14 |
+
|
15 |
+
## Our Standards
|
16 |
+
|
17 |
+
Examples of behavior that contributes to a positive environment for our
|
18 |
+
community include:
|
19 |
+
|
20 |
+
* Demonstrating empathy and kindness toward other people
|
21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
22 |
+
* Giving and gracefully accepting constructive feedback
|
23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
24 |
+
and learning from the experience
|
25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
26 |
+
overall community
|
27 |
+
|
28 |
+
Examples of unacceptable behavior include:
|
29 |
+
|
30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
31 |
+
advances of any kind
|
32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
33 |
+
* Public or private harassment
|
34 |
+
* Publishing others' private information, such as a physical or email
|
35 |
+
address, without their explicit permission
|
36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
37 |
+
professional setting
|
38 |
+
|
39 |
+
## Enforcement Responsibilities
|
40 |
+
|
41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
44 |
+
or harmful.
|
45 |
+
|
46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
49 |
+
decisions when appropriate.
|
50 |
+
|
51 |
+
## Scope
|
52 |
+
|
53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
54 |
+
an individual is officially representing the community in public spaces.
|
55 |
+
Examples of representing our community include using an official e-mail address,
|
56 |
+
posting via an official social media account, or acting as an appointed
|
57 |
+
representative at an online or offline event.
|
58 |
+
|
59 |
+
## Enforcement
|
60 |
+
|
61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
62 |
+
reported to the community leaders responsible for enforcement at
|
63 | |
64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
65 |
+
|
66 |
+
All community leaders are obligated to respect the privacy and security of the
|
67 |
+
reporter of any incident.
|
68 |
+
|
69 |
+
## Enforcement Guidelines
|
70 |
+
|
71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
73 |
+
|
74 |
+
### 1. Correction
|
75 |
+
|
76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
77 |
+
unprofessional or unwelcome in the community.
|
78 |
+
|
79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
80 |
+
clarity around the nature of the violation and an explanation of why the
|
81 |
+
behavior was inappropriate. A public apology may be requested.
|
82 |
+
|
83 |
+
### 2. Warning
|
84 |
+
|
85 |
+
**Community Impact**: A violation through a single incident or series
|
86 |
+
of actions.
|
87 |
+
|
88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
89 |
+
interaction with the people involved, including unsolicited interaction with
|
90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
91 |
+
includes avoiding interactions in community spaces as well as external channels
|
92 |
+
like social media. Violating these terms may lead to a temporary or
|
93 |
+
permanent ban.
|
94 |
+
|
95 |
+
### 3. Temporary Ban
|
96 |
+
|
97 |
+
**Community Impact**: A serious violation of community standards, including
|
98 |
+
sustained inappropriate behavior.
|
99 |
+
|
100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
101 |
+
communication with the community for a specified period of time. No public or
|
102 |
+
private interaction with the people involved, including unsolicited interaction
|
103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
104 |
+
Violating these terms may lead to a permanent ban.
|
105 |
+
|
106 |
+
### 4. Permanent Ban
|
107 |
+
|
108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
111 |
+
|
112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
113 |
+
the community.
|
114 |
+
|
115 |
+
## Attribution
|
116 |
+
|
117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
118 |
+
version 2.0, available at
|
119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
120 |
+
|
121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
123 |
+
|
124 |
+
[homepage]: https://www.contributor-covenant.org
|
125 |
+
|
126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
128 |
+
https://www.contributor-covenant.org/translations.
|
google-calendar-simple-api/.github/ISSUE_TEMPLATE/bug_report.md
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Bug report
|
3 |
+
about: Create a report to help us improve
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
[READ and REMOVE: Please create an issue only if you think that it's something that needs a fix or you have a
|
11 |
+
suggestion/request for improvement. If you have a question or issue not caused by gcsa itself, please use
|
12 |
+
the [discussions page](https://github.com/kuzmoyev/google-calendar-simple-api/discussions).]
|
13 |
+
|
14 |
+
## Bug description
|
15 |
+
|
16 |
+
A clear and concise description of what the bug is.
|
17 |
+
|
18 |
+
## To Reproduce
|
19 |
+
|
20 |
+
Steps to reproduce the behavior:
|
21 |
+
|
22 |
+
1. Installed the latest version with `pip install gcsa`
|
23 |
+
2. ...
|
24 |
+
|
25 |
+
Code used:
|
26 |
+
|
27 |
+
```python
|
28 |
+
from gcsa.google_calendar import GoogleCalendar
|
29 |
+
|
30 |
+
...
|
31 |
+
```
|
32 |
+
|
33 |
+
## Error or unexpected output
|
34 |
+
|
35 |
+
The whole traceback in case of an error:
|
36 |
+
```
|
37 |
+
Traceback (most recent call last):
|
38 |
+
...
|
39 |
+
```
|
40 |
+
|
41 |
+
## Expected behavior
|
42 |
+
|
43 |
+
A clear and concise description of what you expected to happen.
|
44 |
+
|
45 |
+
## Screenshots
|
46 |
+
|
47 |
+
If applicable, add screenshots to help explain your problem.
|
48 |
+
|
49 |
+
## Tech:
|
50 |
+
|
51 |
+
- OS: [e.g. Linux/Windows/MacOS]
|
52 |
+
- GCSA version: [e.g. 2.0.1]
|
53 |
+
- Python version: [e.g. 3.12]
|
54 |
+
|
55 |
+
## Additional context
|
56 |
+
|
57 |
+
Add any other context about the problem here.
|
google-calendar-simple-api/.github/workflows/code-cov.yml
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Code coverage
|
2 |
+
|
3 |
+
on: [pull_request]
|
4 |
+
|
5 |
+
jobs:
|
6 |
+
run:
|
7 |
+
# Don't run on PRs from forks
|
8 |
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
9 |
+
runs-on: ubuntu-latest
|
10 |
+
|
11 |
+
steps:
|
12 |
+
- uses: actions/checkout@v2
|
13 |
+
- name: Set up Python
|
14 |
+
uses: actions/setup-python@v2
|
15 |
+
with:
|
16 |
+
python-version: '3.12'
|
17 |
+
|
18 |
+
- name: Install dependencies
|
19 |
+
run: pip install tox
|
20 |
+
|
21 |
+
- name: Generate code coverage
|
22 |
+
run: tox -e coverage
|
23 |
+
|
24 |
+
|
25 |
+
- name: Post to GitHub
|
26 |
+
uses: 5monkeys/cobertura-action@master
|
27 |
+
with:
|
28 |
+
path: coverage.xml
|
29 |
+
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
30 |
+
minimum_coverage: 75
|
31 |
+
skip_covered: false
|
google-calendar-simple-api/.github/workflows/tests.yml
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Tests
|
2 |
+
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches:
|
6 |
+
- master
|
7 |
+
pull_request:
|
8 |
+
|
9 |
+
jobs:
|
10 |
+
run:
|
11 |
+
|
12 |
+
runs-on: ubuntu-latest
|
13 |
+
strategy:
|
14 |
+
matrix:
|
15 |
+
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]
|
16 |
+
include:
|
17 |
+
- python-version: '3.12'
|
18 |
+
note: with-style-and-docs-checks
|
19 |
+
|
20 |
+
steps:
|
21 |
+
- uses: actions/checkout@v2
|
22 |
+
- name: Set up Python ${{ matrix.python-version }}
|
23 |
+
uses: actions/setup-python@v2
|
24 |
+
with:
|
25 |
+
python-version: ${{ matrix.python-version }}
|
26 |
+
|
27 |
+
- name: Install tox
|
28 |
+
run: pip install tox tox-gh-actions
|
29 |
+
|
30 |
+
- name: Running tests
|
31 |
+
run: tox
|
google-calendar-simple-api/.gitignore
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Created by .ignore support plugin (hsz.mobi)
|
2 |
+
.idea/
|
3 |
+
credentials.json
|
4 |
+
token.pickle
|
5 |
+
venv/
|
6 |
+
__pycache__
|
7 |
+
|
8 |
+
build
|
9 |
+
dist
|
10 |
+
.eggs
|
11 |
+
gcsa.egg-info
|
12 |
+
docs/html
|
13 |
+
|
14 |
+
example.py
|
15 |
+
coverage.xml
|
16 |
+
.coverage
|
17 |
+
.DS_Store
|
18 |
+
.tox
|
google-calendar-simple-api/.readthedocs.yml
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: 2
|
2 |
+
|
3 |
+
build:
|
4 |
+
os: ubuntu-22.04
|
5 |
+
tools:
|
6 |
+
python: "3.12"
|
7 |
+
|
8 |
+
sphinx:
|
9 |
+
configuration: docs/source/conf.py
|
10 |
+
|
11 |
+
python:
|
12 |
+
install:
|
13 |
+
- method: pip
|
14 |
+
path: .
|
15 |
+
extra_requirements:
|
16 |
+
- docs
|
google-calendar-simple-api/CONTRIBUTING.md
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributing to GCSA
|
2 |
+
|
3 |
+
Welcome and thank you for considering contributing to *Google Calendar Simple API* open source project!
|
4 |
+
|
5 |
+
Before contributing to this repository, please first discuss the change you wish to make via
|
6 |
+
[Issue](https://github.com/kuzmoyev/google-calendar-simple-api/issues),
|
7 |
+
[GitHub Discussion](https://github.com/kuzmoyev/google-calendar-simple-api/discussions), or [Discord](https://discord.gg/mRAegbwYKS). Don’t hesitate to ask!
|
8 |
+
Issue submissions, discussions, suggestions are as welcomed contributions as pull requests.
|
9 |
+
|
10 |
+
## Steps to contribute changes
|
11 |
+
|
12 |
+
1. [Fork](https://github.com/kuzmoyev/google-calendar-simple-api/fork) the repository
|
13 |
+
2. Clone it with `git clone [email protected]:{your_username}/google-calendar-simple-api.git`
|
14 |
+
3. Install dependencies if needed with `pip install -e .` (or `pip install -e ".[dev]"` if you want to run tests, compile documentation, etc.).
|
15 |
+
Use [virtualenv](https://virtualenv.pypa.io/en/latest/) to avoid polluting your global python
|
16 |
+
4. Make and commit the changes. Add `closes #{issue_number}` to commit message if applies
|
17 |
+
5. Run the tests with `tox` (these will be run on pull request):
|
18 |
+
* `tox` - all the tests
|
19 |
+
* `tox -e pytest` - unit tests
|
20 |
+
* `tox -e flake8` - style check
|
21 |
+
* `tox -e sphinx` - docs compilation test
|
22 |
+
6. Push
|
23 |
+
7. Create pull request
|
24 |
+
* towards `dev` branch if the changes require a new GCSA version (i.e. changes in [gcsa](https://github.com/kuzmoyev/google-calendar-simple-api/tree/master/gcsa) module)
|
25 |
+
* towards `master` branch if they don't (e.x. changes in README, docs, tests)
|
26 |
+
|
27 |
+
## While contributing
|
28 |
+
|
29 |
+
* Follow the [Code of conduct](https://github.com/kuzmoyev/google-calendar-simple-api/blob/master/.github/CODE_OF_CONDUCT.md)
|
30 |
+
* Follow the [pep8](https://peps.python.org/pep-0008/) and the code style of the project (use your best judgement)
|
31 |
+
* Add documentation of your changes to code and/or to [read-the-docs](https://github.com/kuzmoyev/google-calendar-simple-api/tree/master/docs/source) if needed (use your best judgement)
|
32 |
+
* Add [tests](https://github.com/kuzmoyev/google-calendar-simple-api/tree/master/tests) if needed (use your best judgement)
|
google-calendar-simple-api/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2017
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
google-calendar-simple-api/README.rst
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Google Calendar Simple API
|
2 |
+
==========================
|
3 |
+
|
4 |
+
.. image:: https://badge.fury.io/py/gcsa.svg
|
5 |
+
:target: https://badge.fury.io/py/gcsa
|
6 |
+
:alt: PyPi Package
|
7 |
+
|
8 |
+
.. image:: https://readthedocs.org/projects/google-calendar-simple-api/badge/?version=latest
|
9 |
+
:target: https://google-calendar-simple-api.readthedocs.io/en/latest/?badge=latest
|
10 |
+
:alt: Documentation Status
|
11 |
+
|
12 |
+
.. image:: https://github.com/kuzmoyev/Google-Calendar-Simple-API/workflows/Tests/badge.svg
|
13 |
+
:target: https://github.com/kuzmoyev/Google-Calendar-Simple-API/actions
|
14 |
+
:alt: Tests
|
15 |
+
|
16 |
+
.. image:: https://badgen.net/badge/icon/discord?icon=discord&label
|
17 |
+
:target: https://discord.gg/mRAegbwYKS
|
18 |
+
:alt: Discord
|
19 |
+
|
20 |
+
|
21 |
+
`Google Calendar Simple API` or `gcsa` is a library that simplifies event and calendar management in Google Calendars.
|
22 |
+
It is a Pythonic object oriented adapter for the official API. See the full `documentation`_.
|
23 |
+
|
24 |
+
Installation
|
25 |
+
------------
|
26 |
+
|
27 |
+
.. code-block:: bash
|
28 |
+
|
29 |
+
pip install gcsa
|
30 |
+
|
31 |
+
See `Getting started page`_ for more details and installation options.
|
32 |
+
|
33 |
+
Example usage
|
34 |
+
-------------
|
35 |
+
|
36 |
+
List events
|
37 |
+
~~~~~~~~~~~
|
38 |
+
|
39 |
+
.. code-block:: python
|
40 |
+
|
41 |
+
from gcsa.google_calendar import GoogleCalendar
|
42 |
+
|
43 |
+
calendar = GoogleCalendar('[email protected]')
|
44 |
+
for event in calendar:
|
45 |
+
print(event)
|
46 |
+
|
47 |
+
|
48 |
+
Create event
|
49 |
+
~~~~~~~~~~~~
|
50 |
+
|
51 |
+
.. code-block:: python
|
52 |
+
|
53 |
+
from gcsa.event import Event
|
54 |
+
|
55 |
+
event = Event(
|
56 |
+
'The Glass Menagerie',
|
57 |
+
start=datetime(2020, 7, 10, 19, 0),
|
58 |
+
location='Záhřebská 468/21',
|
59 |
+
minutes_before_popup_reminder=15
|
60 |
+
)
|
61 |
+
calendar.add_event(event)
|
62 |
+
|
63 |
+
|
64 |
+
Create recurring event
|
65 |
+
~~~~~~~~~~~~~~~~~~~~~~
|
66 |
+
|
67 |
+
.. code-block:: python
|
68 |
+
|
69 |
+
from gcsa.recurrence import Recurrence, DAILY
|
70 |
+
|
71 |
+
event = Event(
|
72 |
+
'Breakfast',
|
73 |
+
start=date(2020, 7, 16),
|
74 |
+
recurrence=Recurrence.rule(freq=DAILY)
|
75 |
+
)
|
76 |
+
calendar.add_event(event)
|
77 |
+
|
78 |
+
|
79 |
+
**Suggestion**: use beautiful_date_ to create `date` and `datetime` objects in your
|
80 |
+
projects (*because its beautiful... just like you*).
|
81 |
+
|
82 |
+
|
83 |
+
References
|
84 |
+
----------
|
85 |
+
|
86 |
+
Template for `setup.py` was taken from `kennethreitz/setup.py`_
|
87 |
+
|
88 |
+
|
89 |
+
.. _documentation: https://google-calendar-simple-api.readthedocs.io/en/latest/?badge=latest
|
90 |
+
.. _`Getting started page`: https://google-calendar-simple-api.readthedocs.io/en/latest/getting_started.html
|
91 |
+
.. _beautiful_date: https://github.com/kuzmoyev/beautiful-date
|
92 |
+
.. _`kennethreitz/setup.py`: https://github.com/kennethreitz/setup.py
|
google-calendar-simple-api/build/lib/gcsa/__init__.py
ADDED
File without changes
|
google-calendar-simple-api/build/lib/gcsa/_resource.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from abc import ABC, abstractmethod
|
2 |
+
|
3 |
+
|
4 |
+
class Resource(ABC):
|
5 |
+
@property
|
6 |
+
@abstractmethod
|
7 |
+
def id(self):
|
8 |
+
pass
|
google-calendar-simple-api/build/lib/gcsa/_services/__init__.py
ADDED
File without changes
|
google-calendar-simple-api/build/lib/gcsa/_services/acl_service.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Iterable, Union
|
2 |
+
|
3 |
+
from gcsa._services.base_service import BaseService
|
4 |
+
from gcsa.acl import AccessControlRule
|
5 |
+
from gcsa.serializers.acl_rule_serializer import ACLRuleSerializer
|
6 |
+
|
7 |
+
|
8 |
+
class ACLService(BaseService):
|
9 |
+
"""Access Control List management methods of the `GoogleCalendar`"""
|
10 |
+
|
11 |
+
def get_acl_rules(
|
12 |
+
self,
|
13 |
+
calendar_id: str = None,
|
14 |
+
show_deleted: bool = False
|
15 |
+
) -> Iterable[AccessControlRule]:
|
16 |
+
"""Returns the rules in the access control list for the calendar.
|
17 |
+
|
18 |
+
:param calendar_id:
|
19 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
20 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
21 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
22 |
+
:param show_deleted:
|
23 |
+
Whether to include deleted ACLs in the result. Deleted ACLs are represented by role equal to "none".
|
24 |
+
Deleted ACLs will always be included if syncToken is provided. Optional. The default is False.
|
25 |
+
|
26 |
+
:return:
|
27 |
+
Iterable of `AccessControlRule` objects
|
28 |
+
"""
|
29 |
+
calendar_id = calendar_id or self.default_calendar
|
30 |
+
yield from self._list_paginated(
|
31 |
+
self.service.acl().list,
|
32 |
+
serializer_cls=ACLRuleSerializer,
|
33 |
+
calendarId=calendar_id,
|
34 |
+
**{
|
35 |
+
'showDeleted': show_deleted,
|
36 |
+
}
|
37 |
+
)
|
38 |
+
|
39 |
+
def get_acl_rule(
|
40 |
+
self,
|
41 |
+
rule_id: str,
|
42 |
+
calendar_id: str = None
|
43 |
+
) -> AccessControlRule:
|
44 |
+
"""Returns an access control rule
|
45 |
+
|
46 |
+
:param rule_id:
|
47 |
+
ACL rule identifier.
|
48 |
+
:param calendar_id:
|
49 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
50 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
51 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
52 |
+
|
53 |
+
:return:
|
54 |
+
The corresponding `AccessControlRule` object
|
55 |
+
"""
|
56 |
+
calendar_id = calendar_id or self.default_calendar
|
57 |
+
acl_rule_resource = self.service.acl().get(
|
58 |
+
calendarId=calendar_id,
|
59 |
+
ruleId=rule_id
|
60 |
+
).execute()
|
61 |
+
return ACLRuleSerializer.to_object(acl_rule_resource)
|
62 |
+
|
63 |
+
def add_acl_rule(
|
64 |
+
self,
|
65 |
+
acl_rule: AccessControlRule,
|
66 |
+
send_notifications: bool = True,
|
67 |
+
calendar_id: str = None
|
68 |
+
):
|
69 |
+
"""Adds access control rule
|
70 |
+
|
71 |
+
:param acl_rule:
|
72 |
+
AccessControlRule object.
|
73 |
+
:param send_notifications:
|
74 |
+
Whether to send notifications about the calendar sharing change. The default is True.
|
75 |
+
:param calendar_id:
|
76 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
77 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
78 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
79 |
+
|
80 |
+
:return:
|
81 |
+
Created access control rule with id.
|
82 |
+
"""
|
83 |
+
calendar_id = calendar_id or self.default_calendar
|
84 |
+
body = ACLRuleSerializer.to_json(acl_rule)
|
85 |
+
acl_rule_json = self.service.acl().insert(
|
86 |
+
calendarId=calendar_id,
|
87 |
+
body=body,
|
88 |
+
sendNotifications=send_notifications
|
89 |
+
).execute()
|
90 |
+
return ACLRuleSerializer.to_object(acl_rule_json)
|
91 |
+
|
92 |
+
def update_acl_rule(
|
93 |
+
self,
|
94 |
+
acl_rule: AccessControlRule,
|
95 |
+
send_notifications: bool = True,
|
96 |
+
calendar_id: str = None
|
97 |
+
):
|
98 |
+
"""Updates given access control rule
|
99 |
+
|
100 |
+
:param acl_rule:
|
101 |
+
AccessControlRule object.
|
102 |
+
:param send_notifications:
|
103 |
+
Whether to send notifications about the calendar sharing change. The default is True.
|
104 |
+
:param calendar_id:
|
105 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
106 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
107 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
108 |
+
|
109 |
+
:return:
|
110 |
+
Updated access control rule.
|
111 |
+
"""
|
112 |
+
calendar_id = calendar_id or self.default_calendar
|
113 |
+
acl_id = self._get_resource_id(acl_rule)
|
114 |
+
body = ACLRuleSerializer.to_json(acl_rule)
|
115 |
+
acl_json = self.service.acl().update(
|
116 |
+
calendarId=calendar_id,
|
117 |
+
ruleId=acl_id,
|
118 |
+
body=body,
|
119 |
+
sendNotifications=send_notifications
|
120 |
+
).execute()
|
121 |
+
return ACLRuleSerializer.to_object(acl_json)
|
122 |
+
|
123 |
+
def delete_acl_rule(
|
124 |
+
self,
|
125 |
+
acl_rule: Union[AccessControlRule, str],
|
126 |
+
calendar_id: str = None
|
127 |
+
):
|
128 |
+
"""Deletes access control rule.
|
129 |
+
|
130 |
+
:param acl_rule:
|
131 |
+
Access control rule's ID or `AccessControlRule` object with set `acl_id`.
|
132 |
+
:param calendar_id:
|
133 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
134 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
135 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
136 |
+
"""
|
137 |
+
calendar_id = calendar_id or self.default_calendar
|
138 |
+
acl_id = self._get_resource_id(acl_rule)
|
139 |
+
|
140 |
+
self.service.acl().delete(
|
141 |
+
calendarId=calendar_id,
|
142 |
+
ruleId=acl_id
|
143 |
+
).execute()
|
google-calendar-simple-api/build/lib/gcsa/_services/authentication.py
ADDED
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pickle
|
2 |
+
import os.path
|
3 |
+
import glob
|
4 |
+
from typing import List
|
5 |
+
|
6 |
+
from googleapiclient import discovery
|
7 |
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
8 |
+
from google.auth.transport.requests import Request
|
9 |
+
from google.auth.credentials import Credentials
|
10 |
+
|
11 |
+
|
12 |
+
class AuthenticatedService:
|
13 |
+
"""Handles authentication of the `GoogleCalendar`"""
|
14 |
+
|
15 |
+
_READ_WRITE_SCOPES = 'https://www.googleapis.com/auth/calendar'
|
16 |
+
_LIST_ORDERS = ("startTime", "updated")
|
17 |
+
|
18 |
+
def __init__(
|
19 |
+
self,
|
20 |
+
*,
|
21 |
+
credentials: Credentials = None,
|
22 |
+
credentials_path: str = None,
|
23 |
+
token_path: str = None,
|
24 |
+
save_token: bool = True,
|
25 |
+
read_only: bool = False,
|
26 |
+
authentication_flow_host: str = 'localhost',
|
27 |
+
authentication_flow_port: int = 8080,
|
28 |
+
authentication_flow_bind_addr: str = None
|
29 |
+
):
|
30 |
+
"""
|
31 |
+
Specify ``credentials`` to use in requests or ``credentials_path`` and ``token_path`` to get credentials from.
|
32 |
+
|
33 |
+
:param credentials:
|
34 |
+
Credentials with token and refresh token.
|
35 |
+
If specified, ``credentials_path``, ``token_path``, and ``save_token`` are ignored.
|
36 |
+
If not specified, credentials are retrieved from "token.pickle" file (specified in ``token_path`` or
|
37 |
+
default path) or with authentication flow using secret from "credentials.json" ("client_secret_*.json")
|
38 |
+
(specified in ``credentials_path`` or default path)
|
39 |
+
:param credentials_path:
|
40 |
+
Path to "credentials.json" ("client_secret_*.json") file.
|
41 |
+
Default: ~/.credentials/credentials.json or ~/.credentials/client_secret*.json
|
42 |
+
:param token_path:
|
43 |
+
Existing path to load the token from, or path to save the token after initial authentication flow.
|
44 |
+
Default: "token.pickle" in the same directory as the credentials_path
|
45 |
+
:param save_token:
|
46 |
+
Whether to pickle token after authentication flow for future uses
|
47 |
+
:param read_only:
|
48 |
+
If require read only access. Default: False
|
49 |
+
:param authentication_flow_host:
|
50 |
+
Host to receive response during authentication flow
|
51 |
+
:param authentication_flow_port:
|
52 |
+
Port to receive response during authentication flow
|
53 |
+
:param authentication_flow_bind_addr:
|
54 |
+
Optional IP address for the redirect server to listen on when it is not the same as host
|
55 |
+
(e.g. in a container)
|
56 |
+
"""
|
57 |
+
|
58 |
+
if credentials:
|
59 |
+
self.credentials = self._ensure_refreshed(credentials)
|
60 |
+
else:
|
61 |
+
credentials_path = credentials_path or self._get_default_credentials_path()
|
62 |
+
credentials_dir, credentials_file = os.path.split(credentials_path)
|
63 |
+
token_path = token_path or os.path.join(credentials_dir, 'token.pickle')
|
64 |
+
scopes = [self._READ_WRITE_SCOPES + ('.readonly' if read_only else '')]
|
65 |
+
|
66 |
+
self.credentials = self._get_credentials(
|
67 |
+
token_path,
|
68 |
+
credentials_dir,
|
69 |
+
credentials_file,
|
70 |
+
scopes,
|
71 |
+
save_token,
|
72 |
+
authentication_flow_host,
|
73 |
+
authentication_flow_port,
|
74 |
+
authentication_flow_bind_addr
|
75 |
+
)
|
76 |
+
|
77 |
+
self.service = discovery.build('calendar', 'v3', credentials=self.credentials)
|
78 |
+
|
79 |
+
@staticmethod
|
80 |
+
def _ensure_refreshed(
|
81 |
+
credentials: Credentials
|
82 |
+
) -> Credentials:
|
83 |
+
if not credentials.valid and credentials.expired:
|
84 |
+
credentials.refresh(Request())
|
85 |
+
return credentials
|
86 |
+
|
87 |
+
@staticmethod
|
88 |
+
def _get_credentials(
|
89 |
+
token_path: str,
|
90 |
+
credentials_dir: str,
|
91 |
+
credentials_file: str,
|
92 |
+
scopes: List[str],
|
93 |
+
save_token: bool,
|
94 |
+
host: str,
|
95 |
+
port: int,
|
96 |
+
bind_addr: str
|
97 |
+
) -> Credentials:
|
98 |
+
credentials = None
|
99 |
+
|
100 |
+
if os.path.exists(token_path):
|
101 |
+
with open(token_path, 'rb') as token_file:
|
102 |
+
credentials = pickle.load(token_file)
|
103 |
+
|
104 |
+
if not credentials or not credentials.valid:
|
105 |
+
if credentials and credentials.expired and credentials.refresh_token:
|
106 |
+
credentials.refresh(Request())
|
107 |
+
else:
|
108 |
+
credentials_path = os.path.join(credentials_dir, credentials_file)
|
109 |
+
flow = InstalledAppFlow.from_client_secrets_file(credentials_path, scopes)
|
110 |
+
credentials = flow.run_local_server(host=host, port=port, bind_addr=bind_addr)
|
111 |
+
|
112 |
+
if save_token:
|
113 |
+
with open(token_path, 'wb') as token_file:
|
114 |
+
pickle.dump(credentials, token_file)
|
115 |
+
|
116 |
+
return credentials
|
117 |
+
|
118 |
+
@staticmethod
|
119 |
+
def _get_default_credentials_path() -> str:
|
120 |
+
"""Checks if `.credentials` folder in home directory exists and contains `credentials.json` or
|
121 |
+
`client_secret*.json` file.
|
122 |
+
|
123 |
+
:raises ValueError: if `.credentials` folder does not exist, none of `credentials.json` or `client_secret*.json`
|
124 |
+
files do not exist, or there are multiple `client_secret*.json` files.
|
125 |
+
:return: expanded path to `credentials.json` or `client_secret*.json` file
|
126 |
+
"""
|
127 |
+
home_dir = os.path.expanduser('~')
|
128 |
+
credential_dir = os.path.join(home_dir, '.credentials')
|
129 |
+
if not os.path.exists(credential_dir):
|
130 |
+
raise FileNotFoundError(f'Default credentials directory "{credential_dir}" does not exist.')
|
131 |
+
credential_path = os.path.join(credential_dir, 'credentials.json')
|
132 |
+
if os.path.exists(credential_path):
|
133 |
+
return credential_path
|
134 |
+
else:
|
135 |
+
credentials_files = glob.glob(credential_dir + '/client_secret*.json')
|
136 |
+
if len(credentials_files) > 1:
|
137 |
+
raise ValueError(f"Multiple credential files found in {credential_dir}.\n"
|
138 |
+
f"Try specifying the credentials file, e.x.:\n"
|
139 |
+
f"GoogleCalendar(credentials_path='{credentials_files[0]}')")
|
140 |
+
elif not credentials_files:
|
141 |
+
raise FileNotFoundError(f'Credentials file (credentials.json or client_secret*.json)'
|
142 |
+
f'not found in the default path: "{credential_dir}".')
|
143 |
+
else:
|
144 |
+
return credentials_files[0]
|
google-calendar-simple-api/build/lib/gcsa/_services/base_service.py
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Callable, Type, Union
|
2 |
+
|
3 |
+
from gcsa._resource import Resource
|
4 |
+
from gcsa._services.authentication import AuthenticatedService
|
5 |
+
|
6 |
+
|
7 |
+
class BaseService(AuthenticatedService):
|
8 |
+
def __init__(self, default_calendar, *args, **kwargs):
|
9 |
+
"""
|
10 |
+
:param default_calendar:
|
11 |
+
Users email address or name/id of the calendar. Default: primary calendar of the user
|
12 |
+
|
13 |
+
If user's email or "primary" is specified, then primary calendar of the user is used.
|
14 |
+
You don't need to specify this parameter in this case as it is a default behaviour.
|
15 |
+
|
16 |
+
To use a different calendar you need to specify its id.
|
17 |
+
Go to calendar's `settings and sharing` -> `Integrate calendar` -> `Calendar ID`.
|
18 |
+
"""
|
19 |
+
super().__init__(*args, **kwargs)
|
20 |
+
self.default_calendar = default_calendar
|
21 |
+
|
22 |
+
@staticmethod
|
23 |
+
def _list_paginated(
|
24 |
+
request_method: Callable,
|
25 |
+
serializer_cls: Type = None,
|
26 |
+
**kwargs
|
27 |
+
):
|
28 |
+
page_token = None
|
29 |
+
while True:
|
30 |
+
response_json = request_method(
|
31 |
+
**kwargs,
|
32 |
+
pageToken=page_token
|
33 |
+
).execute()
|
34 |
+
for item_json in response_json['items']:
|
35 |
+
if serializer_cls:
|
36 |
+
yield serializer_cls(item_json).get_object()
|
37 |
+
else:
|
38 |
+
yield item_json
|
39 |
+
page_token = response_json.get('nextPageToken')
|
40 |
+
if not page_token:
|
41 |
+
break
|
42 |
+
|
43 |
+
@staticmethod
|
44 |
+
def _get_resource_id(resource: Union[Resource, str]):
|
45 |
+
"""If `resource` is `Resource` returns its id.
|
46 |
+
If `resource` is string, returns `resource` itself.
|
47 |
+
|
48 |
+
:raises:
|
49 |
+
ValueError: if `resource` is `Resource` object that doesn't have id
|
50 |
+
TypeError: if `resource` is neither `Resource` nor `str`
|
51 |
+
"""
|
52 |
+
if isinstance(resource, Resource):
|
53 |
+
if resource.id is None:
|
54 |
+
raise ValueError("Resource has to have id to be updated, moved or deleted.")
|
55 |
+
return resource.id
|
56 |
+
elif isinstance(resource, str):
|
57 |
+
return resource
|
58 |
+
else:
|
59 |
+
raise TypeError('"resource" object must be Resource or str, not {!r}'.format(
|
60 |
+
resource.__class__.__name__
|
61 |
+
))
|
google-calendar-simple-api/build/lib/gcsa/_services/calendar_lists_service.py
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Iterable, Union
|
2 |
+
|
3 |
+
from gcsa._services.base_service import BaseService
|
4 |
+
from gcsa.calendar import CalendarListEntry, Calendar
|
5 |
+
from gcsa.serializers.calendar_serializer import CalendarListEntrySerializer
|
6 |
+
|
7 |
+
|
8 |
+
class CalendarListService(BaseService):
|
9 |
+
"""Calendar list management methods of the `GoogleCalendar`"""
|
10 |
+
|
11 |
+
def get_calendar_list(
|
12 |
+
self,
|
13 |
+
min_access_role: str = None,
|
14 |
+
show_deleted: bool = False,
|
15 |
+
show_hidden: bool = False
|
16 |
+
) -> Iterable[CalendarListEntry]:
|
17 |
+
"""Returns the calendars on the user's calendar list.
|
18 |
+
|
19 |
+
:param min_access_role:
|
20 |
+
The minimum access role for the user in the returned entries. See :py:class:`~gcsa.calendar.AccessRoles`
|
21 |
+
The default is no restriction.
|
22 |
+
:param show_deleted:
|
23 |
+
Whether to include deleted calendar list entries in the result. The default is False.
|
24 |
+
:param show_hidden:
|
25 |
+
Whether to show hidden entries. The default is False.
|
26 |
+
|
27 |
+
:return:
|
28 |
+
Iterable of :py:class:`~gcsa.calendar.CalendarListEntry` objects.
|
29 |
+
"""
|
30 |
+
yield from self._list_paginated(
|
31 |
+
self.service.calendarList().list,
|
32 |
+
serializer_cls=CalendarListEntrySerializer,
|
33 |
+
minAccessRole=min_access_role,
|
34 |
+
showDeleted=show_deleted,
|
35 |
+
showHidden=show_hidden,
|
36 |
+
)
|
37 |
+
|
38 |
+
def get_calendar_list_entry(
|
39 |
+
self,
|
40 |
+
calendar_id: str = None
|
41 |
+
) -> CalendarListEntry:
|
42 |
+
"""Returns a calendar with the corresponding calendar_id from the user's calendar list.
|
43 |
+
|
44 |
+
:param calendar_id:
|
45 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`
|
46 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
47 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
48 |
+
|
49 |
+
:return:
|
50 |
+
The corresponding :py:class:`~gcsa.calendar.CalendarListEntry` object.
|
51 |
+
"""
|
52 |
+
calendar_id = calendar_id or self.default_calendar
|
53 |
+
calendar_resource = self.service.calendarList().get(calendarId=calendar_id).execute()
|
54 |
+
return CalendarListEntrySerializer.to_object(calendar_resource)
|
55 |
+
|
56 |
+
def add_calendar_list_entry(
|
57 |
+
self,
|
58 |
+
calendar: CalendarListEntry,
|
59 |
+
color_rgb_format: bool = None
|
60 |
+
) -> CalendarListEntry:
|
61 |
+
"""Adds an existing calendar into the user's calendar list.
|
62 |
+
|
63 |
+
:param calendar:
|
64 |
+
:py:class:`~gcsa.calendar.CalendarListEntry` object.
|
65 |
+
:param color_rgb_format:
|
66 |
+
Whether to use the `foreground_color` and `background_color` fields to write the calendar colors (RGB).
|
67 |
+
If this feature is used, the index-based `color_id` field will be set to the best matching option
|
68 |
+
automatically. The default is True if `foreground_color` or `background_color` is set, False otherwise.
|
69 |
+
|
70 |
+
:return:
|
71 |
+
Created `CalendarListEntry` object with id.
|
72 |
+
"""
|
73 |
+
if color_rgb_format is None:
|
74 |
+
color_rgb_format = (calendar.foreground_color is not None) or (calendar.background_color is not None)
|
75 |
+
|
76 |
+
body = CalendarListEntrySerializer.to_json(calendar)
|
77 |
+
calendar_json = self.service.calendarList().insert(
|
78 |
+
body=body,
|
79 |
+
colorRgbFormat=color_rgb_format
|
80 |
+
).execute()
|
81 |
+
return CalendarListEntrySerializer.to_object(calendar_json)
|
82 |
+
|
83 |
+
def update_calendar_list_entry(
|
84 |
+
self,
|
85 |
+
calendar: CalendarListEntry,
|
86 |
+
color_rgb_format: bool = None
|
87 |
+
) -> CalendarListEntry:
|
88 |
+
"""Updates an existing calendar on the user's calendar list.
|
89 |
+
|
90 |
+
:param calendar:
|
91 |
+
:py:class:`~gcsa.calendar.Calendar` object with set `calendar_id`
|
92 |
+
:param color_rgb_format:
|
93 |
+
Whether to use the `foreground_color` and `background_color` fields to write the calendar colors (RGB).
|
94 |
+
If this feature is used, the index-based color_id field will be set to the best matching option
|
95 |
+
automatically. The default is True if `foreground_color` or `background_color` is set, False otherwise.
|
96 |
+
|
97 |
+
:return:
|
98 |
+
Updated calendar list entry object
|
99 |
+
"""
|
100 |
+
calendar_id = self._get_resource_id(calendar)
|
101 |
+
if color_rgb_format is None:
|
102 |
+
color_rgb_format = calendar.foreground_color is not None or calendar.background_color is not None
|
103 |
+
|
104 |
+
body = CalendarListEntrySerializer.to_json(calendar)
|
105 |
+
calendar_json = self.service.calendarList().update(
|
106 |
+
calendarId=calendar_id,
|
107 |
+
body=body,
|
108 |
+
colorRgbFormat=color_rgb_format
|
109 |
+
).execute()
|
110 |
+
return CalendarListEntrySerializer.to_object(calendar_json)
|
111 |
+
|
112 |
+
def delete_calendar_list_entry(
|
113 |
+
self,
|
114 |
+
calendar: Union[Calendar, CalendarListEntry, str]
|
115 |
+
):
|
116 |
+
"""Removes a calendar from the user's calendar list.
|
117 |
+
|
118 |
+
:param calendar:
|
119 |
+
Calendar's ID or :py:class:`~gcsa.calendar.Calendar`/:py:class:`~gcsa.calendar.CalendarListEntry` object
|
120 |
+
with the set `calendar_id`.
|
121 |
+
"""
|
122 |
+
calendar_id = self._get_resource_id(calendar)
|
123 |
+
self.service.calendarList().delete(calendarId=calendar_id).execute()
|
google-calendar-simple-api/build/lib/gcsa/_services/calendars_service.py
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Union
|
2 |
+
|
3 |
+
from gcsa._services.base_service import BaseService
|
4 |
+
from gcsa.calendar import Calendar, CalendarListEntry
|
5 |
+
from gcsa.serializers.calendar_serializer import CalendarSerializer
|
6 |
+
|
7 |
+
|
8 |
+
class CalendarsService(BaseService):
|
9 |
+
"""Calendars management methods of the `GoogleCalendar`"""
|
10 |
+
|
11 |
+
def get_calendar(
|
12 |
+
self,
|
13 |
+
calendar_id: str = None
|
14 |
+
) -> Calendar:
|
15 |
+
"""Returns the calendar with the corresponding calendar_id.
|
16 |
+
|
17 |
+
:param calendar_id:
|
18 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
19 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
20 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
21 |
+
|
22 |
+
:return:
|
23 |
+
The corresponding :py:class:`~gcsa.calendar.Calendar` object.
|
24 |
+
"""
|
25 |
+
calendar_id = calendar_id or self.default_calendar
|
26 |
+
calendar_resource = self.service.calendars().get(
|
27 |
+
calendarId=calendar_id
|
28 |
+
).execute()
|
29 |
+
return CalendarSerializer.to_object(calendar_resource)
|
30 |
+
|
31 |
+
def add_calendar(
|
32 |
+
self,
|
33 |
+
calendar: Calendar
|
34 |
+
):
|
35 |
+
"""Creates a secondary calendar.
|
36 |
+
|
37 |
+
:param calendar:
|
38 |
+
Calendar object.
|
39 |
+
:return:
|
40 |
+
Created calendar object with ID.
|
41 |
+
"""
|
42 |
+
body = CalendarSerializer.to_json(calendar)
|
43 |
+
calendar_json = self.service.calendars().insert(
|
44 |
+
body=body
|
45 |
+
).execute()
|
46 |
+
return CalendarSerializer.to_object(calendar_json)
|
47 |
+
|
48 |
+
def update_calendar(
|
49 |
+
self,
|
50 |
+
calendar: Calendar
|
51 |
+
):
|
52 |
+
"""Updates metadata for a calendar.
|
53 |
+
|
54 |
+
:param calendar:
|
55 |
+
Calendar object with set `calendar_id`
|
56 |
+
|
57 |
+
:return:
|
58 |
+
Updated calendar object
|
59 |
+
"""
|
60 |
+
calendar_id = self._get_resource_id(calendar)
|
61 |
+
body = CalendarSerializer.to_json(calendar)
|
62 |
+
calendar_json = self.service.calendars().update(
|
63 |
+
calendarId=calendar_id,
|
64 |
+
body=body
|
65 |
+
).execute()
|
66 |
+
return CalendarSerializer.to_object(calendar_json)
|
67 |
+
|
68 |
+
def delete_calendar(
|
69 |
+
self,
|
70 |
+
calendar: Union[Calendar, CalendarListEntry, str]
|
71 |
+
):
|
72 |
+
"""Deletes a secondary calendar.
|
73 |
+
|
74 |
+
Use :py:meth:`~gcsa.google_calendar.GoogleCalendar.clear_calendar` for clearing all events on primary calendars.
|
75 |
+
|
76 |
+
:param calendar:
|
77 |
+
Calendar's ID or :py:class:`~gcsa.calendar.Calendar` object with set `calendar_id`.
|
78 |
+
"""
|
79 |
+
calendar_id = self._get_resource_id(calendar)
|
80 |
+
self.service.calendars().delete(calendarId=calendar_id).execute()
|
81 |
+
|
82 |
+
def clear_calendar(self):
|
83 |
+
"""Clears a **primary** calendar.
|
84 |
+
This operation deletes all events associated with the **primary** calendar of an account.
|
85 |
+
|
86 |
+
Currently, there is no way to clear a secondary calendar.
|
87 |
+
You can use :py:meth:`~gcsa.google_calendar.GoogleCalendar.delete_event` method with the secondary calendar's ID
|
88 |
+
to delete events from a secondary calendar.
|
89 |
+
"""
|
90 |
+
self.service.calendars().clear(calendarId='primary').execute()
|
91 |
+
|
92 |
+
def clear(self):
|
93 |
+
"""Kept for back-compatibility. Use :py:meth:`~gcsa.google_calendar.GoogleCalendar.clear_calendar` instead.
|
94 |
+
|
95 |
+
Clears a **primary** calendar.
|
96 |
+
This operation deletes all events associated with the **primary** calendar of an account.
|
97 |
+
|
98 |
+
Currently, there is no way to clear a secondary calendar.
|
99 |
+
You can use :py:meth:`~gcsa.google_calendar.GoogleCalendar.delete_event` method with the secondary calendar's ID
|
100 |
+
to delete events from a secondary calendar.
|
101 |
+
"""
|
102 |
+
self.clear_calendar()
|
google-calendar-simple-api/build/lib/gcsa/_services/colors_service.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from gcsa._services.base_service import BaseService
|
2 |
+
|
3 |
+
|
4 |
+
class ColorsService(BaseService):
|
5 |
+
"""Colors management methods of the `GoogleCalendar`"""
|
6 |
+
|
7 |
+
def list_event_colors(self) -> dict:
|
8 |
+
"""A global palette of event colors, mapping from the color ID to its definition.
|
9 |
+
An :py:class:`~gcsa.event.Event` may refer to one of these color IDs in its color_id field."""
|
10 |
+
return self.service.colors().get().execute()['event']
|
11 |
+
|
12 |
+
def list_calendar_colors(self) -> dict:
|
13 |
+
"""A global palette of calendar colors, mapping from the color ID to its definition.
|
14 |
+
:py:class:`~gcsa.calendar.CalendarListEntry` resource refers to one of these color IDs in its color_id field."""
|
15 |
+
return self.service.colors().get().execute()['calendar']
|
google-calendar-simple-api/build/lib/gcsa/_services/events_service.py
ADDED
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import date, datetime
|
2 |
+
from typing import Union, Iterator, Iterable, Callable
|
3 |
+
|
4 |
+
from beautiful_date import BeautifulDate
|
5 |
+
from dateutil.relativedelta import relativedelta
|
6 |
+
from tzlocal import get_localzone_name
|
7 |
+
|
8 |
+
from gcsa._services.base_service import BaseService
|
9 |
+
from gcsa.event import Event
|
10 |
+
from gcsa.serializers.event_serializer import EventSerializer
|
11 |
+
from gcsa.util.date_time_util import to_localized_iso
|
12 |
+
|
13 |
+
|
14 |
+
class SendUpdatesMode:
|
15 |
+
"""Possible values of the mode for sending updates or invitations to attendees.
|
16 |
+
|
17 |
+
* ALL - Send updates to all participants. This is the default value.
|
18 |
+
* EXTERNAL_ONLY - Send updates only to attendees not using google calendar.
|
19 |
+
* NONE - Do not send updates.
|
20 |
+
"""
|
21 |
+
|
22 |
+
ALL = "all"
|
23 |
+
EXTERNAL_ONLY = "externalOnly"
|
24 |
+
NONE = "none"
|
25 |
+
|
26 |
+
|
27 |
+
class EventsService(BaseService):
|
28 |
+
"""Event management methods of the `GoogleCalendar`"""
|
29 |
+
|
30 |
+
def _list_events(
|
31 |
+
self,
|
32 |
+
request_method: Callable,
|
33 |
+
time_min: Union[date, datetime, BeautifulDate],
|
34 |
+
time_max: Union[date, datetime, BeautifulDate],
|
35 |
+
timezone: str,
|
36 |
+
calendar_id: str,
|
37 |
+
**kwargs
|
38 |
+
) -> Iterable[Event]:
|
39 |
+
"""Lists paginated events received from request_method."""
|
40 |
+
|
41 |
+
time_min = time_min or datetime.now()
|
42 |
+
time_max = time_max or time_min + relativedelta(years=1)
|
43 |
+
|
44 |
+
time_min = to_localized_iso(time_min, timezone)
|
45 |
+
time_max = to_localized_iso(time_max, timezone)
|
46 |
+
|
47 |
+
yield from self._list_paginated(
|
48 |
+
request_method,
|
49 |
+
serializer_cls=EventSerializer,
|
50 |
+
calendarId=calendar_id,
|
51 |
+
timeMin=time_min,
|
52 |
+
timeMax=time_max,
|
53 |
+
**kwargs
|
54 |
+
)
|
55 |
+
|
56 |
+
def get_events(
|
57 |
+
self,
|
58 |
+
time_min: Union[date, datetime, BeautifulDate] = None,
|
59 |
+
time_max: Union[date, datetime, BeautifulDate] = None,
|
60 |
+
order_by: str = None,
|
61 |
+
timezone: str = get_localzone_name(),
|
62 |
+
single_events: bool = False,
|
63 |
+
query: str = None,
|
64 |
+
calendar_id: str = None,
|
65 |
+
**kwargs
|
66 |
+
) -> Iterable[Event]:
|
67 |
+
"""Lists events.
|
68 |
+
|
69 |
+
:param time_min:
|
70 |
+
Staring date/datetime
|
71 |
+
:param time_max:
|
72 |
+
Ending date/datetime
|
73 |
+
:param order_by:
|
74 |
+
Order of the events. Possible values: "startTime", "updated". Default is unspecified stable order.
|
75 |
+
:param timezone:
|
76 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
77 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
78 |
+
:param single_events:
|
79 |
+
Whether to expand recurring events into instances and only return single one-off events and
|
80 |
+
instances of recurring events, but not the underlying recurring events themselves.
|
81 |
+
:param query:
|
82 |
+
Free text search terms to find events that match these terms in any field, except for
|
83 |
+
extended properties.
|
84 |
+
:param calendar_id:
|
85 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
86 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
87 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
88 |
+
:param kwargs:
|
89 |
+
Additional API parameters.
|
90 |
+
See https://developers.google.com/calendar/v3/reference/events/list#optional-parameters
|
91 |
+
|
92 |
+
:return:
|
93 |
+
Iterable of `Event` objects
|
94 |
+
"""
|
95 |
+
calendar_id = calendar_id or self.default_calendar
|
96 |
+
if not single_events and order_by == 'startTime':
|
97 |
+
raise ValueError(
|
98 |
+
'"startTime" ordering is only available when querying single events, i.e. single_events=True'
|
99 |
+
)
|
100 |
+
yield from self._list_events(
|
101 |
+
self.service.events().list,
|
102 |
+
time_min=time_min,
|
103 |
+
time_max=time_max,
|
104 |
+
timezone=timezone,
|
105 |
+
calendar_id=calendar_id,
|
106 |
+
**{
|
107 |
+
'singleEvents': single_events,
|
108 |
+
'orderBy': order_by,
|
109 |
+
'q': query,
|
110 |
+
**kwargs
|
111 |
+
}
|
112 |
+
)
|
113 |
+
|
114 |
+
def get_instances(
|
115 |
+
self,
|
116 |
+
recurring_event: Union[Event, str],
|
117 |
+
time_min: Union[date, datetime, BeautifulDate] = None,
|
118 |
+
time_max: Union[date, datetime, BeautifulDate] = None,
|
119 |
+
timezone: str = get_localzone_name(),
|
120 |
+
calendar_id: str = None,
|
121 |
+
**kwargs
|
122 |
+
) -> Iterable[Event]:
|
123 |
+
"""Lists instances of recurring event
|
124 |
+
|
125 |
+
:param recurring_event:
|
126 |
+
Recurring event or instance of recurring event (`Event` object) or id of the recurring event
|
127 |
+
:param time_min:
|
128 |
+
Staring date/datetime
|
129 |
+
:param time_max:
|
130 |
+
Ending date/datetime
|
131 |
+
:param timezone:
|
132 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
133 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
134 |
+
:param calendar_id:
|
135 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
136 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
137 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
138 |
+
:param kwargs:
|
139 |
+
Additional API parameters.
|
140 |
+
See https://developers.google.com/calendar/v3/reference/events/instances#optional-parameters
|
141 |
+
|
142 |
+
:return:
|
143 |
+
Iterable of event objects
|
144 |
+
"""
|
145 |
+
calendar_id = calendar_id or self.default_calendar
|
146 |
+
try:
|
147 |
+
event_id = self._get_resource_id(recurring_event)
|
148 |
+
except ValueError:
|
149 |
+
raise ValueError("Recurring event has to have id to retrieve its instances.")
|
150 |
+
|
151 |
+
yield from self._list_events(
|
152 |
+
self.service.events().instances,
|
153 |
+
time_min=time_min,
|
154 |
+
time_max=time_max,
|
155 |
+
timezone=timezone,
|
156 |
+
calendar_id=calendar_id,
|
157 |
+
**{
|
158 |
+
'eventId': event_id,
|
159 |
+
**kwargs
|
160 |
+
}
|
161 |
+
)
|
162 |
+
|
163 |
+
def __iter__(self) -> Iterator[Event]:
|
164 |
+
return iter(self.get_events())
|
165 |
+
|
166 |
+
def __getitem__(self, r):
|
167 |
+
if isinstance(r, slice):
|
168 |
+
time_min, time_max, order_by = r.start or None, r.stop or None, r.step or None
|
169 |
+
elif isinstance(r, (date, datetime)):
|
170 |
+
time_min, time_max, order_by = r, None, None
|
171 |
+
else:
|
172 |
+
raise NotImplementedError
|
173 |
+
|
174 |
+
if (
|
175 |
+
(time_min and not isinstance(time_min, (date, datetime)))
|
176 |
+
or (time_max and not isinstance(time_max, (date, datetime)))
|
177 |
+
or (order_by and (not isinstance(order_by, str) or order_by not in self._LIST_ORDERS))
|
178 |
+
):
|
179 |
+
raise ValueError('Calendar indexing is in the following format: time_min[:time_max[:order_by]],'
|
180 |
+
' where time_min and time_max are date/datetime objects'
|
181 |
+
' and order_by is None or one of "startTime" or "updated" strings.')
|
182 |
+
|
183 |
+
return self.get_events(time_min, time_max, order_by=order_by, single_events=(order_by == "startTime"))
|
184 |
+
|
185 |
+
def get_event(
|
186 |
+
self,
|
187 |
+
event_id: str,
|
188 |
+
calendar_id: str = None,
|
189 |
+
**kwargs
|
190 |
+
) -> Event:
|
191 |
+
"""Returns the event with the corresponding event_id.
|
192 |
+
|
193 |
+
:param event_id:
|
194 |
+
The unique event ID.
|
195 |
+
:param kwargs:
|
196 |
+
Additional API parameters.
|
197 |
+
See https://developers.google.com/calendar/v3/reference/events/get#optional-parameters
|
198 |
+
:param calendar_id:
|
199 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
200 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
201 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
202 |
+
|
203 |
+
:return:
|
204 |
+
The corresponding event object.
|
205 |
+
"""
|
206 |
+
calendar_id = calendar_id or self.default_calendar
|
207 |
+
event_resource = self.service.events().get(
|
208 |
+
calendarId=calendar_id,
|
209 |
+
eventId=event_id,
|
210 |
+
**kwargs
|
211 |
+
).execute()
|
212 |
+
return EventSerializer.to_object(event_resource)
|
213 |
+
|
214 |
+
def add_event(
|
215 |
+
self,
|
216 |
+
event: Event,
|
217 |
+
send_updates: str = SendUpdatesMode.NONE,
|
218 |
+
calendar_id: str = None,
|
219 |
+
**kwargs
|
220 |
+
) -> Event:
|
221 |
+
"""Creates event in the calendar
|
222 |
+
|
223 |
+
:param event:
|
224 |
+
Event object.
|
225 |
+
:param send_updates:
|
226 |
+
Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
|
227 |
+
Default is "NONE".
|
228 |
+
:param calendar_id:
|
229 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
230 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
231 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
232 |
+
:param kwargs:
|
233 |
+
Additional API parameters.
|
234 |
+
See https://developers.google.com/calendar/v3/reference/events/insert#optional-parameters
|
235 |
+
|
236 |
+
:return:
|
237 |
+
Created event object with id.
|
238 |
+
"""
|
239 |
+
calendar_id = calendar_id or self.default_calendar
|
240 |
+
body = EventSerializer.to_json(event)
|
241 |
+
event_json = self.service.events().insert(
|
242 |
+
calendarId=calendar_id,
|
243 |
+
body=body,
|
244 |
+
conferenceDataVersion=1,
|
245 |
+
sendUpdates=send_updates,
|
246 |
+
**kwargs
|
247 |
+
).execute()
|
248 |
+
return EventSerializer.to_object(event_json)
|
249 |
+
|
250 |
+
def add_quick_event(
|
251 |
+
self,
|
252 |
+
event_string: str,
|
253 |
+
send_updates: str = SendUpdatesMode.NONE,
|
254 |
+
calendar_id: str = None,
|
255 |
+
**kwargs
|
256 |
+
) -> Event:
|
257 |
+
"""Creates event in the calendar by string description.
|
258 |
+
|
259 |
+
Example:
|
260 |
+
Appointment at Somewhere on June 3rd 10am-10:25am
|
261 |
+
|
262 |
+
:param event_string:
|
263 |
+
String that describes an event
|
264 |
+
:param send_updates:
|
265 |
+
Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
|
266 |
+
Default is "NONE".
|
267 |
+
:param calendar_id:
|
268 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
269 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
270 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
271 |
+
:param kwargs:
|
272 |
+
Additional API parameters.
|
273 |
+
See https://developers.google.com/calendar/v3/reference/events/quickAdd#optional-parameters
|
274 |
+
|
275 |
+
:return:
|
276 |
+
Created event object with id.
|
277 |
+
"""
|
278 |
+
calendar_id = calendar_id or self.default_calendar
|
279 |
+
event_json = self.service.events().quickAdd(
|
280 |
+
calendarId=calendar_id,
|
281 |
+
text=event_string,
|
282 |
+
sendUpdates=send_updates,
|
283 |
+
**kwargs
|
284 |
+
).execute()
|
285 |
+
return EventSerializer.to_object(event_json)
|
286 |
+
|
287 |
+
def update_event(
|
288 |
+
self,
|
289 |
+
event: Event,
|
290 |
+
send_updates: str = SendUpdatesMode.NONE,
|
291 |
+
calendar_id: str = None,
|
292 |
+
**kwargs
|
293 |
+
) -> Event:
|
294 |
+
"""Updates existing event in the calendar
|
295 |
+
|
296 |
+
:param event:
|
297 |
+
Event object with set `event_id`.
|
298 |
+
:param send_updates:
|
299 |
+
Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
|
300 |
+
Default is "NONE".
|
301 |
+
:param calendar_id:
|
302 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
303 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
304 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
305 |
+
:param kwargs:
|
306 |
+
Additional API parameters.
|
307 |
+
See https://developers.google.com/calendar/v3/reference/events/update#optional-parameters
|
308 |
+
|
309 |
+
:return:
|
310 |
+
Updated event object.
|
311 |
+
"""
|
312 |
+
calendar_id = calendar_id or self.default_calendar
|
313 |
+
event_id = self._get_resource_id(event)
|
314 |
+
body = EventSerializer.to_json(event)
|
315 |
+
event_json = self.service.events().update(
|
316 |
+
calendarId=calendar_id,
|
317 |
+
eventId=event_id,
|
318 |
+
body=body,
|
319 |
+
conferenceDataVersion=1,
|
320 |
+
sendUpdates=send_updates,
|
321 |
+
**kwargs
|
322 |
+
).execute()
|
323 |
+
return EventSerializer.to_object(event_json)
|
324 |
+
|
325 |
+
def import_event(
|
326 |
+
self,
|
327 |
+
event: Event,
|
328 |
+
calendar_id: str = None,
|
329 |
+
**kwargs
|
330 |
+
) -> Event:
|
331 |
+
"""Imports an event in the calendar
|
332 |
+
|
333 |
+
This operation is used to add a private copy of an existing event to a calendar.
|
334 |
+
|
335 |
+
:param event:
|
336 |
+
Event object.
|
337 |
+
:param calendar_id:
|
338 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
339 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
340 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
341 |
+
:param kwargs:
|
342 |
+
Additional API parameters.
|
343 |
+
See https://developers.google.com/calendar/v3/reference/events/import#optional-parameters
|
344 |
+
|
345 |
+
:return:
|
346 |
+
Created event object with id.
|
347 |
+
"""
|
348 |
+
calendar_id = calendar_id or self.default_calendar
|
349 |
+
body = EventSerializer.to_json(event)
|
350 |
+
event_json = self.service.events().import_(
|
351 |
+
calendarId=calendar_id,
|
352 |
+
body=body,
|
353 |
+
conferenceDataVersion=1,
|
354 |
+
**kwargs
|
355 |
+
).execute()
|
356 |
+
return EventSerializer.to_object(event_json)
|
357 |
+
|
358 |
+
def move_event(
|
359 |
+
self,
|
360 |
+
event: Event,
|
361 |
+
destination_calendar_id: str,
|
362 |
+
send_updates: str = SendUpdatesMode.NONE,
|
363 |
+
source_calendar_id: str = None,
|
364 |
+
**kwargs
|
365 |
+
) -> Event:
|
366 |
+
"""Moves existing event from calendar to another calendar
|
367 |
+
|
368 |
+
:param event:
|
369 |
+
Event object with set event_id.
|
370 |
+
:param destination_calendar_id:
|
371 |
+
ID of the destination calendar.
|
372 |
+
:param send_updates:
|
373 |
+
Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
|
374 |
+
Default is "NONE".
|
375 |
+
:param source_calendar_id:
|
376 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
377 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
378 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
379 |
+
:param kwargs:
|
380 |
+
Additional API parameters.
|
381 |
+
See https://developers.google.com/calendar/v3/reference/events/move#optional-parameters
|
382 |
+
|
383 |
+
:return:
|
384 |
+
Moved event object.
|
385 |
+
"""
|
386 |
+
source_calendar_id = source_calendar_id or self.default_calendar
|
387 |
+
event_id = self._get_resource_id(event)
|
388 |
+
moved_event_json = self.service.events().move(
|
389 |
+
calendarId=source_calendar_id,
|
390 |
+
eventId=event_id,
|
391 |
+
destination=destination_calendar_id,
|
392 |
+
sendUpdates=send_updates,
|
393 |
+
**kwargs
|
394 |
+
).execute()
|
395 |
+
return EventSerializer.to_object(moved_event_json)
|
396 |
+
|
397 |
+
def delete_event(
|
398 |
+
self,
|
399 |
+
event: Union[Event, str],
|
400 |
+
send_updates: str = SendUpdatesMode.NONE,
|
401 |
+
calendar_id: str = None,
|
402 |
+
**kwargs
|
403 |
+
):
|
404 |
+
"""Deletes an event.
|
405 |
+
|
406 |
+
:param event:
|
407 |
+
Event's ID or `Event` object with set `event_id`.
|
408 |
+
:param send_updates:
|
409 |
+
Whether and how to send updates to attendees. See :py:class:`~gcsa.google_calendar.SendUpdatesMode`
|
410 |
+
Default is "NONE".
|
411 |
+
:param calendar_id:
|
412 |
+
Calendar identifier. Default is `default_calendar` specified in `GoogleCalendar`.
|
413 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
414 |
+
If you want to access the primary calendar of the currently logged-in user, use the "primary" keyword.
|
415 |
+
:param kwargs:
|
416 |
+
Additional API parameters.
|
417 |
+
See https://developers.google.com/calendar/v3/reference/events/delete#optional-parameters
|
418 |
+
"""
|
419 |
+
calendar_id = calendar_id or self.default_calendar
|
420 |
+
event_id = self._get_resource_id(event)
|
421 |
+
|
422 |
+
self.service.events().delete(
|
423 |
+
calendarId=calendar_id,
|
424 |
+
eventId=event_id,
|
425 |
+
sendUpdates=send_updates,
|
426 |
+
**kwargs
|
427 |
+
).execute()
|
google-calendar-simple-api/build/lib/gcsa/_services/free_busy_service.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import date, datetime
|
2 |
+
from typing import Union, List
|
3 |
+
|
4 |
+
from beautiful_date import BeautifulDate
|
5 |
+
from dateutil.relativedelta import relativedelta
|
6 |
+
from tzlocal import get_localzone_name
|
7 |
+
|
8 |
+
from gcsa._services.base_service import BaseService
|
9 |
+
from gcsa.free_busy import FreeBusy, FreeBusyQueryError
|
10 |
+
from gcsa.serializers.free_busy_serializer import FreeBusySerializer
|
11 |
+
from gcsa.util.date_time_util import to_localized_iso
|
12 |
+
|
13 |
+
|
14 |
+
class FreeBusyService(BaseService):
|
15 |
+
def get_free_busy(
|
16 |
+
self,
|
17 |
+
resource_ids: Union[str, List[str]] = None,
|
18 |
+
*,
|
19 |
+
time_min: Union[date, datetime, BeautifulDate] = None,
|
20 |
+
time_max: Union[date, datetime, BeautifulDate] = None,
|
21 |
+
timezone: str = get_localzone_name(),
|
22 |
+
group_expansion_max: int = None,
|
23 |
+
calendar_expansion_max: int = None,
|
24 |
+
ignore_errors: bool = False
|
25 |
+
) -> FreeBusy:
|
26 |
+
"""Returns free/busy information for a set of calendars and/or groups.
|
27 |
+
|
28 |
+
:param resource_ids:
|
29 |
+
Identifier or list of identifiers of calendar(s) and/or group(s).
|
30 |
+
Default is `default_calendar` specified in `GoogleCalendar`.
|
31 |
+
:param time_min:
|
32 |
+
The start of the interval for the query.
|
33 |
+
:param time_max:
|
34 |
+
The end of the interval for the query.
|
35 |
+
:param timezone:
|
36 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
37 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
38 |
+
:param group_expansion_max:
|
39 |
+
Maximal number of calendar identifiers to be provided for a single group.
|
40 |
+
An error is returned for a group with more members than this value.
|
41 |
+
Maximum value is 100.
|
42 |
+
:param calendar_expansion_max:
|
43 |
+
Maximal number of calendars for which FreeBusy information is to be provided.
|
44 |
+
Maximum value is 50.
|
45 |
+
:param ignore_errors:
|
46 |
+
Whether errors related to calendars and/or groups should be ignored.
|
47 |
+
If `False` :py:class:`~gcsa.free_busy.FreeBusyQueryError` is raised in case of query related errors.
|
48 |
+
If `True`, related errors are stored in the resulting :py:class:`~gcsa.free_busy.FreeBusy` object.
|
49 |
+
Default is `False`.
|
50 |
+
Note, request related errors (e.x. authentication error) will not be ignored regardless of
|
51 |
+
the `ignore_errors` value.
|
52 |
+
|
53 |
+
:return:
|
54 |
+
:py:class:`~gcsa.free_busy.FreeBusy` object.
|
55 |
+
"""
|
56 |
+
|
57 |
+
time_min = time_min or datetime.now()
|
58 |
+
time_max = time_max or time_min + relativedelta(weeks=2)
|
59 |
+
|
60 |
+
time_min = to_localized_iso(time_min, timezone)
|
61 |
+
time_max = to_localized_iso(time_max, timezone)
|
62 |
+
|
63 |
+
if resource_ids is None:
|
64 |
+
resource_ids = [self.default_calendar]
|
65 |
+
elif not isinstance(resource_ids, (list, tuple, set)):
|
66 |
+
resource_ids = [resource_ids]
|
67 |
+
|
68 |
+
body = {
|
69 |
+
"timeMin": time_min,
|
70 |
+
"timeMax": time_max,
|
71 |
+
"timeZone": timezone,
|
72 |
+
"groupExpansionMax": group_expansion_max,
|
73 |
+
"calendarExpansionMax": calendar_expansion_max,
|
74 |
+
"items": [
|
75 |
+
{
|
76 |
+
"id": r_id
|
77 |
+
} for r_id in resource_ids
|
78 |
+
]
|
79 |
+
}
|
80 |
+
|
81 |
+
free_busy_json = self.service.freebusy().query(body=body).execute()
|
82 |
+
free_busy = FreeBusySerializer.to_object(free_busy_json)
|
83 |
+
if not ignore_errors and (free_busy.groups_errors or free_busy.calendars_errors):
|
84 |
+
raise FreeBusyQueryError(groups_errors=free_busy.groups_errors,
|
85 |
+
calendars_errors=free_busy.calendars_errors)
|
86 |
+
|
87 |
+
return free_busy
|
google-calendar-simple-api/build/lib/gcsa/_services/settings_service.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from gcsa._services.base_service import BaseService
|
2 |
+
from gcsa.serializers.settings_serializer import SettingsSerializer
|
3 |
+
from gcsa.settings import Settings
|
4 |
+
|
5 |
+
|
6 |
+
class SettingsService(BaseService):
|
7 |
+
"""Settings management methods of the `GoogleCalendar`"""
|
8 |
+
|
9 |
+
def get_settings(self) -> Settings:
|
10 |
+
"""Returns user settings for the authenticated user."""
|
11 |
+
settings_list = list(self._list_paginated(self.service.settings().list))
|
12 |
+
settings_json = {s['id']: s['value'] for s in settings_list}
|
13 |
+
return SettingsSerializer.to_object(settings_json)
|
google-calendar-simple-api/build/lib/gcsa/acl.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from gcsa._resource import Resource
|
2 |
+
|
3 |
+
|
4 |
+
class ACLRole:
|
5 |
+
"""
|
6 |
+
* `NONE` - Provides no access.
|
7 |
+
* `FREE_BUSY_READER` - Provides read access to free/busy information.
|
8 |
+
* `READER` - Provides read access to the calendar. Private events will appear to users with reader access, but event
|
9 |
+
details will be hidden.
|
10 |
+
* `WRITER` - Provides read and write access to the calendar. Private events will appear to users with writer access,
|
11 |
+
and event details will be visible.
|
12 |
+
* `OWNER` - Provides ownership of the calendar. This role has all of the permissions of the writer role with
|
13 |
+
the additional ability to see and manipulate ACLs.
|
14 |
+
"""
|
15 |
+
|
16 |
+
NONE = "none"
|
17 |
+
FREE_BUSY_READER = "freeBusyReader"
|
18 |
+
READER = "reader"
|
19 |
+
WRITER = "writer"
|
20 |
+
OWNER = "owner"
|
21 |
+
|
22 |
+
|
23 |
+
class ACLScopeType:
|
24 |
+
"""
|
25 |
+
* `DEFAULT` - The public scope.
|
26 |
+
* `USER` - Limits the scope to a single user.
|
27 |
+
* `GROUP` - Limits the scope to a group.
|
28 |
+
* `DOMAIN` - Limits the scope to a domain.
|
29 |
+
"""
|
30 |
+
|
31 |
+
DEFAULT = "default"
|
32 |
+
USER = "user"
|
33 |
+
GROUP = "group"
|
34 |
+
DOMAIN = "domain"
|
35 |
+
|
36 |
+
|
37 |
+
class AccessControlRule(Resource):
|
38 |
+
def __init__(
|
39 |
+
self,
|
40 |
+
*,
|
41 |
+
role: str,
|
42 |
+
scope_type: str,
|
43 |
+
acl_id: str = None,
|
44 |
+
scope_value: str = None
|
45 |
+
):
|
46 |
+
"""
|
47 |
+
:param role:
|
48 |
+
The role assigned to the scope. See :py:class:`~gcsa.acl.ACLRole`.
|
49 |
+
:param scope_type:
|
50 |
+
The type of the scope. See :py:class:`~gcsa.acl.ACLScopeType`.
|
51 |
+
:param acl_id:
|
52 |
+
Identifier of the Access Control List (ACL) rule.
|
53 |
+
:param scope_value:
|
54 |
+
The email address of a user or group, or the name of a domain, depending on the scope type.
|
55 |
+
Omitted for type "default".
|
56 |
+
"""
|
57 |
+
self.acl_id = acl_id
|
58 |
+
self.role = role
|
59 |
+
self.scope_type = scope_type
|
60 |
+
self.scope_value = scope_value
|
61 |
+
|
62 |
+
@property
|
63 |
+
def id(self):
|
64 |
+
return self.acl_id
|
65 |
+
|
66 |
+
def __str__(self):
|
67 |
+
return '{} - {}'.format(self.scope_value, self.role)
|
68 |
+
|
69 |
+
def __repr__(self):
|
70 |
+
return '<AccessControlRule {}>'.format(self.__str__())
|
google-calendar-simple-api/build/lib/gcsa/attachment.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Attachment:
|
2 |
+
_SUPPORTED_MIME_TYPES = {
|
3 |
+
"application/vnd.google-apps.audio",
|
4 |
+
"application/vnd.google-apps.document", # Google Docs
|
5 |
+
"application/vnd.google-apps.drawing", # Google Drawing
|
6 |
+
"application/vnd.google-apps.file", # Google Drive file
|
7 |
+
"application/vnd.google-apps.folder", # Google Drive folder
|
8 |
+
"application/vnd.google-apps.form", # Google Forms
|
9 |
+
"application/vnd.google-apps.fusiontable", # Google Fusion Tables
|
10 |
+
"application/vnd.google-apps.map", # Google My Maps
|
11 |
+
"application/vnd.google-apps.photo",
|
12 |
+
"application/vnd.google-apps.presentation", # Google Slides
|
13 |
+
"application/vnd.google-apps.script", # Google Apps Scripts
|
14 |
+
"application/vnd.google-apps.site", # Google Sites
|
15 |
+
"application/vnd.google-apps.spreadsheet", # Google Sheets
|
16 |
+
"application/vnd.google-apps.unknown",
|
17 |
+
"application/vnd.google-apps.video",
|
18 |
+
"application/vnd.google-apps.drive-sdk" # 3rd party shortcut
|
19 |
+
}
|
20 |
+
|
21 |
+
def __init__(
|
22 |
+
self,
|
23 |
+
file_url: str,
|
24 |
+
title: str = None,
|
25 |
+
mime_type: str = None,
|
26 |
+
_icon_link: str = None,
|
27 |
+
_file_id: str = None
|
28 |
+
):
|
29 |
+
"""File attachment for the event.
|
30 |
+
|
31 |
+
Currently only Google Drive attachments are supported.
|
32 |
+
|
33 |
+
:param file_url:
|
34 |
+
A link for opening the file in a relevant Google editor or viewer.
|
35 |
+
:param title:
|
36 |
+
Attachment title
|
37 |
+
:param mime_type:
|
38 |
+
Internet media type (MIME type) of the attachment. See `available MIME types`_
|
39 |
+
:param _icon_link:
|
40 |
+
URL link to the attachment's icon (read only)
|
41 |
+
:param _file_id:
|
42 |
+
Id of the attached file (read only)
|
43 |
+
|
44 |
+
.. note: "read only" means that Attachment has given property only
|
45 |
+
when received from the existing event in the calendar.
|
46 |
+
|
47 |
+
.. _`available MIME types`: https://developers.google.com/drive/api/v3/mime-types
|
48 |
+
"""
|
49 |
+
|
50 |
+
self.unsupported_mime_type = mime_type not in Attachment._SUPPORTED_MIME_TYPES
|
51 |
+
|
52 |
+
self.file_url = file_url
|
53 |
+
self.title = title
|
54 |
+
self.mime_type = mime_type
|
55 |
+
self.icon_link = _icon_link
|
56 |
+
self.file_id = _file_id
|
57 |
+
|
58 |
+
def __eq__(self, other):
|
59 |
+
return (
|
60 |
+
isinstance(other, Attachment)
|
61 |
+
and self.file_url == other.file_url
|
62 |
+
and self.title == other.title
|
63 |
+
and self.mime_type == other.mime_type
|
64 |
+
and self.icon_link == other.icon_link
|
65 |
+
and self.file_id == other.file_id
|
66 |
+
)
|
67 |
+
|
68 |
+
def __str__(self):
|
69 |
+
return "'{}' - '{}'".format(self.title, self.file_url)
|
70 |
+
|
71 |
+
def __repr__(self):
|
72 |
+
return '<Attachment {}>'.format(self.__str__())
|
google-calendar-simple-api/build/lib/gcsa/attendee.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .person import Person
|
2 |
+
|
3 |
+
|
4 |
+
class ResponseStatus:
|
5 |
+
"""Possible values for attendee's response status
|
6 |
+
|
7 |
+
* NEEDS_ACTION - The attendee has not responded to the invitation.
|
8 |
+
* DECLINED - The attendee has declined the invitation.
|
9 |
+
* TENTATIVE - The attendee has tentatively accepted the invitation.
|
10 |
+
* ACCEPTED - The attendee has accepted the invitation.
|
11 |
+
"""
|
12 |
+
NEEDS_ACTION = "needsAction"
|
13 |
+
DECLINED = "declined"
|
14 |
+
TENTATIVE = "tentative"
|
15 |
+
ACCEPTED = "accepted"
|
16 |
+
|
17 |
+
|
18 |
+
class Attendee(Person):
|
19 |
+
def __init__(
|
20 |
+
self,
|
21 |
+
email: str,
|
22 |
+
display_name: str = None,
|
23 |
+
comment: str = None,
|
24 |
+
optional: bool = None,
|
25 |
+
is_resource: bool = None,
|
26 |
+
additional_guests: int = None,
|
27 |
+
_id: str = None,
|
28 |
+
_is_self: bool = None,
|
29 |
+
_response_status: str = None
|
30 |
+
):
|
31 |
+
"""Represents attendee of the event.
|
32 |
+
|
33 |
+
:param email:
|
34 |
+
The attendee's email address, if available.
|
35 |
+
:param display_name:
|
36 |
+
The attendee's name, if available
|
37 |
+
:param comment:
|
38 |
+
The attendee's response comment
|
39 |
+
:param optional:
|
40 |
+
Whether this is an optional attendee. The default is False.
|
41 |
+
:param is_resource:
|
42 |
+
Whether the attendee is a resource.
|
43 |
+
Can only be set when the attendee is added to the event
|
44 |
+
for the first time. Subsequent modifications are ignored.
|
45 |
+
The default is False.
|
46 |
+
:param additional_guests:
|
47 |
+
Number of additional guests. The default is 0.
|
48 |
+
:param _id:
|
49 |
+
The attendee's Profile ID, if available.
|
50 |
+
It corresponds to the id field in the People collection of the Google+ API
|
51 |
+
:param _is_self:
|
52 |
+
Whether this entry represents the calendar on which this copy of the event appears.
|
53 |
+
The default is False (set by Google's API).
|
54 |
+
:param _response_status:
|
55 |
+
The attendee's response status. See :py:class:`~gcsa.attendee.ResponseStatus`
|
56 |
+
"""
|
57 |
+
super().__init__(email=email, display_name=display_name, _id=_id, _is_self=_is_self)
|
58 |
+
self.comment = comment
|
59 |
+
self.optional = optional
|
60 |
+
self.is_resource = is_resource
|
61 |
+
self.additional_guests = additional_guests
|
62 |
+
self.response_status = _response_status
|
63 |
+
|
64 |
+
def __eq__(self, other):
|
65 |
+
return (
|
66 |
+
isinstance(other, Attendee)
|
67 |
+
and super().__eq__(other)
|
68 |
+
and self.comment == other.comment
|
69 |
+
and self.optional == other.optional
|
70 |
+
and self.is_resource == other.is_resource
|
71 |
+
and self.additional_guests == other.additional_guests
|
72 |
+
and self.response_status == other.response_status
|
73 |
+
)
|
74 |
+
|
75 |
+
def __str__(self):
|
76 |
+
return "'{}' - response: '{}'".format(self.email, self.response_status)
|
77 |
+
|
78 |
+
def __repr__(self):
|
79 |
+
return '<Attendee {}>'.format(self.__str__())
|
google-calendar-simple-api/build/lib/gcsa/calendar.py
ADDED
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
|
3 |
+
from tzlocal import get_localzone_name
|
4 |
+
|
5 |
+
from ._resource import Resource
|
6 |
+
from .reminders import Reminder
|
7 |
+
|
8 |
+
|
9 |
+
class NotificationType:
|
10 |
+
"""
|
11 |
+
* `EVENT_CREATION` - Notification sent when a new event is put on the calendar.
|
12 |
+
* `EVENT_CHANGE` - Notification sent when an event is changed.
|
13 |
+
* `EVENT_CANCELLATION` - Notification sent when an event is cancelled.
|
14 |
+
* `EVENT_RESPONSE` - Notification sent when an attendee responds to the event invitation.
|
15 |
+
* `AGENDA` - An agenda with the events of the day (sent out in the morning).
|
16 |
+
"""
|
17 |
+
|
18 |
+
EVENT_CREATION = "eventCreation"
|
19 |
+
EVENT_CHANGE = "eventChange"
|
20 |
+
EVENT_CANCELLATION = "eventCancellation"
|
21 |
+
EVENT_RESPONSE = "eventResponse"
|
22 |
+
AGENDA = "agenda"
|
23 |
+
|
24 |
+
|
25 |
+
class AccessRoles:
|
26 |
+
"""
|
27 |
+
* `FREE_BUSY_READER` - Provides read access to free/busy information.
|
28 |
+
* `READER` - Provides read access to the calendar.
|
29 |
+
Private events will appear to users with reader access, but event details will be hidden.
|
30 |
+
* `WRITER` - Provides read and write access to the calendar.
|
31 |
+
Private events will appear to users with writer access, and event details will be visible.
|
32 |
+
* `OWNER` - Provides ownership of the calendar.
|
33 |
+
This role has all of the permissions of the writer role with the additional ability to see and manipulate ACLs.
|
34 |
+
"""
|
35 |
+
|
36 |
+
FREE_BUSY_READER = "freeBusyReader"
|
37 |
+
READER = "reader"
|
38 |
+
WRITER = "writer"
|
39 |
+
OWNER = "owner"
|
40 |
+
|
41 |
+
|
42 |
+
class Calendar(Resource):
|
43 |
+
def __init__(
|
44 |
+
self,
|
45 |
+
summary: str,
|
46 |
+
*,
|
47 |
+
calendar_id: str = None,
|
48 |
+
description: str = None,
|
49 |
+
location: str = None,
|
50 |
+
timezone: str = get_localzone_name(),
|
51 |
+
allowed_conference_solution_types: List[str] = None
|
52 |
+
):
|
53 |
+
"""
|
54 |
+
:param summary:
|
55 |
+
Title of the calendar.
|
56 |
+
:param calendar_id:
|
57 |
+
Identifier of the calendar.
|
58 |
+
To retrieve calendar IDs call the :py:meth:`~gcsa.google_calendar.GoogleCalendar.get_calendar_list`.
|
59 |
+
:param description:
|
60 |
+
Description of the calendar.
|
61 |
+
:param location:
|
62 |
+
Geographic location of the calendar as free-form text.
|
63 |
+
:param timezone:
|
64 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
65 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
66 |
+
:param allowed_conference_solution_types:
|
67 |
+
The types of conference solutions that are supported for this calendar.
|
68 |
+
See :py:class:`~gcsa.conference.SolutionType`
|
69 |
+
"""
|
70 |
+
self.summary = summary
|
71 |
+
self.calendar_id = calendar_id
|
72 |
+
self.description = description
|
73 |
+
self.location = location
|
74 |
+
self.timezone = timezone
|
75 |
+
self.allowed_conference_solution_types = allowed_conference_solution_types
|
76 |
+
|
77 |
+
@property
|
78 |
+
def id(self):
|
79 |
+
return self.calendar_id
|
80 |
+
|
81 |
+
def to_calendar_list_entry(
|
82 |
+
self,
|
83 |
+
summary_override: str = None,
|
84 |
+
color_id: str = None,
|
85 |
+
background_color: str = None,
|
86 |
+
foreground_color: str = None,
|
87 |
+
hidden: bool = False,
|
88 |
+
selected: bool = False,
|
89 |
+
default_reminders: List[Reminder] = None,
|
90 |
+
notification_types: List[str] = None,
|
91 |
+
) -> 'CalendarListEntry':
|
92 |
+
"""Converts :py:class:`~gcsa.calendar.Calendar` to :py:class:`~gcsa.calendar.CalendarListEntry`
|
93 |
+
that can be added to the calendar list.
|
94 |
+
|
95 |
+
:py:class:`~gcsa.calendar.Calendar` has to have `calendar_id` set
|
96 |
+
to be converted to :py:class:`~gcsa.calendar.CalendarListEntry`
|
97 |
+
|
98 |
+
:param summary_override:
|
99 |
+
The summary that the authenticated user has set for this calendar.
|
100 |
+
:param color_id:
|
101 |
+
The color of the calendar. This is an ID referring to an entry in the calendar section of the colors'
|
102 |
+
definition (See :py:meth:`~gcsa.google_calendar.GoogleCalendar.list_calendar_colors`).
|
103 |
+
This property is superseded by the `background_color` and `foreground_color` properties
|
104 |
+
and can be ignored when using these properties.
|
105 |
+
:param background_color:
|
106 |
+
The main color of the calendar in the hexadecimal format "#0088aa".
|
107 |
+
This property supersedes the index-based color_id property.
|
108 |
+
:param foreground_color:
|
109 |
+
The foreground color of the calendar in the hexadecimal format "#ffffff".
|
110 |
+
This property supersedes the index-based color_id property.
|
111 |
+
:param hidden:
|
112 |
+
Whether the calendar has been hidden from the list.
|
113 |
+
:param selected:
|
114 |
+
Whether the calendar content shows up in the calendar UI. The default is False.
|
115 |
+
:param default_reminders:
|
116 |
+
The default reminders that the authenticated user has for this calendar. :py:mod:`~gcsa.reminders`
|
117 |
+
:param notification_types:
|
118 |
+
The list of notification types set for this calendar. :py:class:`~gcsa:calendar:NotificationType`
|
119 |
+
|
120 |
+
:return:
|
121 |
+
:py:class:`~gcsa.calendar.CalendarListEntry` object that can be added to the calendar list.
|
122 |
+
"""
|
123 |
+
if self.id is None:
|
124 |
+
raise ValueError('Calendar has to have `calendar_id` set to be converted to CalendarListEntry')
|
125 |
+
|
126 |
+
return CalendarListEntry(
|
127 |
+
_summary=self.summary,
|
128 |
+
calendar_id=self.calendar_id,
|
129 |
+
_description=self.description,
|
130 |
+
_location=self.location,
|
131 |
+
_timezone=self.timezone,
|
132 |
+
_allowed_conference_solution_types=self.allowed_conference_solution_types,
|
133 |
+
|
134 |
+
summary_override=summary_override,
|
135 |
+
color_id=color_id,
|
136 |
+
background_color=background_color,
|
137 |
+
foreground_color=foreground_color,
|
138 |
+
hidden=hidden,
|
139 |
+
selected=selected,
|
140 |
+
default_reminders=default_reminders,
|
141 |
+
notification_types=notification_types,
|
142 |
+
)
|
143 |
+
|
144 |
+
def __str__(self):
|
145 |
+
return '{} - {}'.format(self.summary, self.description)
|
146 |
+
|
147 |
+
def __repr__(self):
|
148 |
+
return '<Calendar {}>'.format(self.__str__())
|
149 |
+
|
150 |
+
def __eq__(self, other):
|
151 |
+
if not isinstance(other, Calendar):
|
152 |
+
return NotImplemented
|
153 |
+
elif self is other:
|
154 |
+
return True
|
155 |
+
else:
|
156 |
+
return super().__eq__(other)
|
157 |
+
|
158 |
+
|
159 |
+
class CalendarListEntry(Calendar):
|
160 |
+
def __init__(
|
161 |
+
self,
|
162 |
+
calendar_id: str,
|
163 |
+
*,
|
164 |
+
summary_override: str = None,
|
165 |
+
color_id: str = None,
|
166 |
+
background_color: str = None,
|
167 |
+
foreground_color: str = None,
|
168 |
+
hidden: bool = False,
|
169 |
+
selected: bool = False,
|
170 |
+
default_reminders: List[Reminder] = None,
|
171 |
+
notification_types: List[str] = None,
|
172 |
+
_summary: str = None,
|
173 |
+
_description: str = None,
|
174 |
+
_location: str = None,
|
175 |
+
_timezone: str = None,
|
176 |
+
_allowed_conference_solution_types: List[str] = None,
|
177 |
+
_access_role: str = None,
|
178 |
+
_primary: bool = False,
|
179 |
+
_deleted: bool = False
|
180 |
+
):
|
181 |
+
"""
|
182 |
+
:param calendar_id:
|
183 |
+
Identifier of the calendar.
|
184 |
+
:param summary_override:
|
185 |
+
The summary that the authenticated user has set for this calendar.
|
186 |
+
:param color_id:
|
187 |
+
The color of the calendar. This is an ID referring to an entry in the calendar section of the colors'
|
188 |
+
definition (See :py:meth:`~gcsa.google_calendar.GoogleCalendar.list_calendar_colors`).
|
189 |
+
This property is superseded by the `background_color` and `foreground_color` properties
|
190 |
+
and can be ignored when using these properties.
|
191 |
+
:param background_color:
|
192 |
+
The main color of the calendar in the hexadecimal format "#0088aa".
|
193 |
+
This property supersedes the index-based color_id property.
|
194 |
+
:param foreground_color:
|
195 |
+
The foreground color of the calendar in the hexadecimal format "#ffffff".
|
196 |
+
This property supersedes the index-based color_id property.
|
197 |
+
:param hidden:
|
198 |
+
Whether the calendar has been hidden from the list.
|
199 |
+
:param selected:
|
200 |
+
Whether the calendar content shows up in the calendar UI. The default is False.
|
201 |
+
:param default_reminders:
|
202 |
+
The default reminders that the authenticated user has for this calendar. :py:mod:`~gcsa.reminders`
|
203 |
+
:param notification_types:
|
204 |
+
The list of notification types set for this calendar. :py:class:`~gcsa:calendar:NotificationType`
|
205 |
+
:param _summary:
|
206 |
+
Title of the calendar. Read-only.
|
207 |
+
:param _description:
|
208 |
+
Description of the calendar. Read-only.
|
209 |
+
:param _location:
|
210 |
+
Geographic location of the calendar as free-form text. Read-only.
|
211 |
+
:param _timezone:
|
212 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". Read-only.
|
213 |
+
:param _allowed_conference_solution_types:
|
214 |
+
The types of conference solutions that are supported for this calendar. Read-only.
|
215 |
+
See :py:class:`~gcsa.conference.SolutionType`
|
216 |
+
:param _access_role:
|
217 |
+
The effective access role that the authenticated user has on the calendar. Read-only.
|
218 |
+
See :py:class:`~gcsa.calendar.AccessRoles`
|
219 |
+
:param _primary:
|
220 |
+
Whether the calendar is the primary calendar of the authenticated user. Read-only.
|
221 |
+
:param _deleted:
|
222 |
+
Whether this calendar list entry has been deleted from the calendar list. Read-only.
|
223 |
+
"""
|
224 |
+
super().__init__(
|
225 |
+
summary=_summary,
|
226 |
+
calendar_id=calendar_id,
|
227 |
+
description=_description,
|
228 |
+
location=_location,
|
229 |
+
timezone=_timezone,
|
230 |
+
allowed_conference_solution_types=_allowed_conference_solution_types
|
231 |
+
)
|
232 |
+
self.summary_override = summary_override
|
233 |
+
self._color_id = color_id
|
234 |
+
self.background_color = background_color
|
235 |
+
self.foreground_color = foreground_color
|
236 |
+
self.hidden = hidden
|
237 |
+
self.selected = selected
|
238 |
+
self.default_reminders = default_reminders
|
239 |
+
self.notification_types = notification_types
|
240 |
+
self.access_role = _access_role
|
241 |
+
self.primary = _primary
|
242 |
+
self.deleted = _deleted
|
243 |
+
|
244 |
+
@property
|
245 |
+
def color_id(self):
|
246 |
+
return self._color_id
|
247 |
+
|
248 |
+
@color_id.setter
|
249 |
+
def color_id(self, color_id):
|
250 |
+
"""Sets the color_id and resets background_color and foreground_color."""
|
251 |
+
self._color_id = color_id
|
252 |
+
self.background_color = None
|
253 |
+
self.foreground_color = None
|
254 |
+
|
255 |
+
def __str__(self):
|
256 |
+
return '{} - ({})'.format(self.summary_override, self.summary)
|
257 |
+
|
258 |
+
def __repr__(self):
|
259 |
+
return '<CalendarListEntry {}>'.format(self.__str__())
|
260 |
+
|
261 |
+
def __eq__(self, other):
|
262 |
+
if not isinstance(other, CalendarListEntry):
|
263 |
+
return NotImplemented
|
264 |
+
elif self is other:
|
265 |
+
return True
|
266 |
+
else:
|
267 |
+
return super().__eq__(other)
|
google-calendar-simple-api/build/lib/gcsa/conference.py
ADDED
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Union, List
|
2 |
+
from uuid import uuid4
|
3 |
+
|
4 |
+
|
5 |
+
class SolutionType:
|
6 |
+
"""
|
7 |
+
* HANGOUT - for Hangouts for consumers (hangouts.google.com)
|
8 |
+
* NAMED_HANGOUT - for classic Hangouts for Google Workspace users (hangouts.google.com)
|
9 |
+
* HANGOUTS_MEET - for Google Meet (meet.google.com)
|
10 |
+
* ADD_ON - for 3P conference providers
|
11 |
+
"""
|
12 |
+
|
13 |
+
HANGOUT = 'eventHangout'
|
14 |
+
NAMED_HANGOUT = 'eventNamedHangout'
|
15 |
+
HANGOUTS_MEET = 'hangoutsMeet'
|
16 |
+
ADD_ON = 'addOn'
|
17 |
+
|
18 |
+
|
19 |
+
class _BaseConferenceSolution:
|
20 |
+
"""General conference-related information."""
|
21 |
+
|
22 |
+
def __init__(
|
23 |
+
self,
|
24 |
+
conference_id: str = None,
|
25 |
+
signature: str = None,
|
26 |
+
notes: str = None,
|
27 |
+
_status: str = 'success'
|
28 |
+
):
|
29 |
+
"""
|
30 |
+
:param conference_id:
|
31 |
+
The ID of the conference. Optional.
|
32 |
+
Can be used by developers to keep track of conferences, should not be displayed to users.
|
33 |
+
|
34 |
+
Values for solution types (see :py:class:`~gcsa.conference.SolutionType`):
|
35 |
+
|
36 |
+
* HANGOUT: unset
|
37 |
+
* NAMED_HANGOUT: the name of the Hangout
|
38 |
+
* HANGOUTS_MEET: the 10-letter meeting code, for example "aaa-bbbb-ccc"
|
39 |
+
* ADD_ON: defined by 3P conference provider
|
40 |
+
|
41 |
+
:param signature:
|
42 |
+
The signature of the conference data.
|
43 |
+
Generated on server side. Must be preserved while copying the conference data between events,
|
44 |
+
otherwise the conference data will not be copied.
|
45 |
+
None for a conference with a failed create request.
|
46 |
+
Optional for a conference with a pending create request.
|
47 |
+
:param notes:
|
48 |
+
String of additional notes (such as instructions from the domain administrator, legal notices)
|
49 |
+
to display to the user. Can contain HTML. The maximum length is 2048 characters
|
50 |
+
|
51 |
+
:param _status:
|
52 |
+
The current status of the conference create request. Should not be set by developer.
|
53 |
+
|
54 |
+
The possible values are:
|
55 |
+
|
56 |
+
* "pending": the conference create request is still being processed.
|
57 |
+
* "failure": the conference create request failed, there are no entry points.
|
58 |
+
* "success": the conference create request succeeded, the entry points are populated.
|
59 |
+
In this case `ConferenceSolution` with created entry points
|
60 |
+
is stored in the event's `conference_data`. And `ConferenceSolutionCreateRequest` is omitted.
|
61 |
+
|
62 |
+
Create requests are asynchronous. Check ``status`` field of event's ``conference_solution`` to find it's
|
63 |
+
status. If the status is ``"success"``, ``conference_solution`` will contain a
|
64 |
+
:py:class:`~gcsa.conference.ConferenceSolution` object and you'll be able to access it's field (like
|
65 |
+
``entry_points``). Otherwise (if ``status`` is ``""pending"`` or ``"failure"``), ``conference_solution``
|
66 |
+
will contain a :py:class:`~gcsa.conference.ConferenceSolutionCreateRequest` object.
|
67 |
+
|
68 |
+
"""
|
69 |
+
if notes and len(notes) > 2048:
|
70 |
+
raise ValueError('Maximum notes length is 2048 characters.')
|
71 |
+
|
72 |
+
self.conference_id = conference_id
|
73 |
+
self.signature = signature
|
74 |
+
self.notes = notes
|
75 |
+
self.status = _status
|
76 |
+
|
77 |
+
def __eq__(self, other):
|
78 |
+
if not isinstance(other, _BaseConferenceSolution):
|
79 |
+
return NotImplemented
|
80 |
+
elif self is other:
|
81 |
+
return True
|
82 |
+
else:
|
83 |
+
return (
|
84 |
+
self.conference_id == other.conference_id
|
85 |
+
and self.signature == other.signature
|
86 |
+
and self.notes == other.notes
|
87 |
+
)
|
88 |
+
|
89 |
+
|
90 |
+
class EntryPoint:
|
91 |
+
"""Information about individual conference entry points, such as URLs or phone numbers."""
|
92 |
+
|
93 |
+
VIDEO = 'video'
|
94 |
+
PHONE = 'phone'
|
95 |
+
SIP = 'sip'
|
96 |
+
MORE = 'more'
|
97 |
+
|
98 |
+
ENTRY_POINT_TYPES = (VIDEO, PHONE, SIP, MORE)
|
99 |
+
|
100 |
+
def __init__(
|
101 |
+
self,
|
102 |
+
entry_point_type: str,
|
103 |
+
uri: str = None,
|
104 |
+
label: str = None,
|
105 |
+
pin: str = None,
|
106 |
+
access_code: str = None,
|
107 |
+
meeting_code: str = None,
|
108 |
+
passcode: str = None,
|
109 |
+
password: str = None
|
110 |
+
):
|
111 |
+
"""
|
112 |
+
When creating new conference data, populate only the subset of `meeting_code`, `access_code`, `passcode`,
|
113 |
+
`password`, and `pin` fields that match the terminology that the conference provider uses.
|
114 |
+
|
115 |
+
Only the populated fields should be displayed.
|
116 |
+
|
117 |
+
:param entry_point_type:
|
118 |
+
The type of the conference entry point.
|
119 |
+
|
120 |
+
Possible values are:
|
121 |
+
|
122 |
+
* VIDEO - joining a conference over HTTP.
|
123 |
+
A conference can have zero or one `VIDEO` entry point.
|
124 |
+
* PHONE - joining a conference by dialing a phone number.
|
125 |
+
A conference can have zero or more `PHONE` entry points.
|
126 |
+
* SIP - joining a conference over SIP.
|
127 |
+
A conference can have zero or one `SIP` entry point.
|
128 |
+
* MORE - further conference joining instructions, for example additional phone numbers.
|
129 |
+
A conference can have zero or one `MORE` entry point.
|
130 |
+
A conference with only a `MORE` entry point is not a valid conference.
|
131 |
+
|
132 |
+
:param uri:
|
133 |
+
The URI of the entry point. The maximum length is 1300 characters.
|
134 |
+
Format:
|
135 |
+
|
136 |
+
* for `VIDEO`, http: or https: schema is required.
|
137 |
+
* for `PHONE`, tel: schema is required.
|
138 |
+
The URI should include the entire dial sequence (e.g., tel:+12345678900,,,123456789;1234).
|
139 |
+
* for `SIP`, sip: schema is required, e.g., sip:[email protected].
|
140 |
+
* for `MORE`, http: or https: schema is required.
|
141 |
+
|
142 |
+
:param label:
|
143 |
+
The label for the URI.
|
144 |
+
Visible to end users. Not localized. The maximum length is 512 characters.
|
145 |
+
|
146 |
+
Examples:
|
147 |
+
|
148 |
+
* for `VIDEO`: meet.google.com/aaa-bbbb-ccc
|
149 |
+
* for `PHONE`: +1 123 268 2601
|
150 |
+
* for `SIP`: [email protected]
|
151 |
+
* for `MORE`: should not be filled
|
152 |
+
|
153 |
+
:param pin:
|
154 |
+
The PIN to access the conference. The maximum length is 128 characters.
|
155 |
+
:param access_code:
|
156 |
+
The access code to access the conference. The maximum length is 128 characters. Optional.
|
157 |
+
:param meeting_code:
|
158 |
+
The meeting code to access the conference. The maximum length is 128 characters.
|
159 |
+
:param passcode:
|
160 |
+
The passcode to access the conference. The maximum length is 128 characters.
|
161 |
+
:param password:
|
162 |
+
The password to access the conference. The maximum length is 128 characters.
|
163 |
+
"""
|
164 |
+
|
165 |
+
if entry_point_type and entry_point_type not in self.ENTRY_POINT_TYPES:
|
166 |
+
raise ValueError('"entry_point" must be one of {}. {} was provided.'.format(
|
167 |
+
', '.join(self.ENTRY_POINT_TYPES),
|
168 |
+
entry_point_type
|
169 |
+
))
|
170 |
+
if label and len(label) > 512:
|
171 |
+
raise ValueError('Maximum label length is 512 characters.')
|
172 |
+
if pin and len(pin) > 128:
|
173 |
+
raise ValueError('Maximum pin length is 128 characters.')
|
174 |
+
if access_code and len(access_code) > 128:
|
175 |
+
raise ValueError('Maximum access_code length is 128 characters.')
|
176 |
+
if meeting_code and len(meeting_code) > 128:
|
177 |
+
raise ValueError('Maximum meeting_code length is 128 characters.')
|
178 |
+
if passcode and len(passcode) > 128:
|
179 |
+
raise ValueError('Maximum passcode length is 128 characters.')
|
180 |
+
if password and len(password) > 128:
|
181 |
+
raise ValueError('Maximum password length is 128 characters.')
|
182 |
+
|
183 |
+
self.entry_point_type = entry_point_type
|
184 |
+
self.uri = uri
|
185 |
+
self.label = label
|
186 |
+
self.pin = pin
|
187 |
+
self.access_code = access_code
|
188 |
+
self.meeting_code = meeting_code
|
189 |
+
self.passcode = passcode
|
190 |
+
self.password = password
|
191 |
+
|
192 |
+
def __eq__(self, other):
|
193 |
+
if not isinstance(other, EntryPoint):
|
194 |
+
return NotImplemented
|
195 |
+
elif self is other:
|
196 |
+
return True
|
197 |
+
else:
|
198 |
+
return (
|
199 |
+
self.entry_point_type == other.entry_point_type
|
200 |
+
and self.uri == other.uri
|
201 |
+
and self.label == other.label
|
202 |
+
and self.pin == other.pin
|
203 |
+
and self.access_code == other.access_code
|
204 |
+
and self.meeting_code == other.meeting_code
|
205 |
+
and self.passcode == other.passcode
|
206 |
+
and self.password == other.password
|
207 |
+
)
|
208 |
+
|
209 |
+
def __str__(self):
|
210 |
+
return "{} - '{}'".format(self.entry_point_type, self.uri)
|
211 |
+
|
212 |
+
def __repr__(self):
|
213 |
+
return '<EntryPoint {}>'.format(self.__str__())
|
214 |
+
|
215 |
+
|
216 |
+
class ConferenceSolution(_BaseConferenceSolution):
|
217 |
+
"""Information about the conference solution, such as Hangouts or Google Meet."""
|
218 |
+
|
219 |
+
def __init__(
|
220 |
+
self,
|
221 |
+
entry_points: Union[EntryPoint, List[EntryPoint]],
|
222 |
+
solution_type: str = None,
|
223 |
+
name: str = None,
|
224 |
+
icon_uri: str = None,
|
225 |
+
conference_id: str = None,
|
226 |
+
signature: str = None,
|
227 |
+
notes: str = None
|
228 |
+
):
|
229 |
+
"""
|
230 |
+
:param entry_points:
|
231 |
+
:py:class:`~gcsa.conference.EntryPoint` or list of :py:class:`~gcsa.conference.EntryPoint` s.
|
232 |
+
Information about individual conference entry points, such as URLs or phone numbers.
|
233 |
+
All of them must belong to the same conference.
|
234 |
+
:param solution_type:
|
235 |
+
Solution type. See :py:class:`~gcsa.conference.SolutionType`
|
236 |
+
|
237 |
+
The possible values are:
|
238 |
+
|
239 |
+
* HANGOUT - for Hangouts for consumers (hangouts.google.com)
|
240 |
+
* NAMED_HANGOUT - for classic Hangouts for Google Workspace users (hangouts.google.com)
|
241 |
+
* HANGOUTS_MEET - for Google Meet (meet.google.com)
|
242 |
+
* ADD_ON - for 3P conference providers
|
243 |
+
|
244 |
+
:param name:
|
245 |
+
The user-visible name of this solution. Not localized.
|
246 |
+
:param icon_uri:
|
247 |
+
The user-visible icon for this solution.
|
248 |
+
:param conference_id:
|
249 |
+
The ID of the conference. Optional.
|
250 |
+
Can be used by developers to keep track of conferences, should not be displayed to users.
|
251 |
+
|
252 |
+
Values for solution types (see :py:class:`~gcsa.conference.SolutionType`):
|
253 |
+
|
254 |
+
* HANGOUT: unset
|
255 |
+
* NAMED_HANGOUT: the name of the Hangout
|
256 |
+
* HANGOUTS_MEET: the 10-letter meeting code, for example "aaa-bbbb-ccc"
|
257 |
+
* ADD_ON: defined by 3P conference provider
|
258 |
+
|
259 |
+
:param signature:
|
260 |
+
The signature of the conference data.
|
261 |
+
Generated on server side. Must be preserved while copying the conference data between events,
|
262 |
+
otherwise the conference data will not be copied.
|
263 |
+
None for a conference with a failed create request.
|
264 |
+
Optional for a conference with a pending create request.
|
265 |
+
:param notes:
|
266 |
+
String of additional notes (such as instructions from the domain administrator, legal notices)
|
267 |
+
to display to the user. Can contain HTML. The maximum length is 2048 characters
|
268 |
+
"""
|
269 |
+
super().__init__(conference_id=conference_id, signature=signature, notes=notes)
|
270 |
+
|
271 |
+
self.entry_points = [entry_points] if isinstance(entry_points, EntryPoint) else entry_points
|
272 |
+
self._check_entry_points()
|
273 |
+
|
274 |
+
self.solution_type = solution_type
|
275 |
+
self.name = name
|
276 |
+
self.icon_uri = icon_uri
|
277 |
+
|
278 |
+
def _check_entry_points(self):
|
279 |
+
"""
|
280 |
+
Checks counts of entry points types.
|
281 |
+
|
282 |
+
* A conference can have zero or one `VIDEO` entry point.
|
283 |
+
* A conference can have zero or more `PHONE` entry points.
|
284 |
+
* A conference can have zero or one `SIP` entry point.
|
285 |
+
* A conference can have zero or one `MORE` entry point.
|
286 |
+
A conference with only a `MORE` entry point is not a valid conference.
|
287 |
+
"""
|
288 |
+
if len(self.entry_points) == 0:
|
289 |
+
raise ValueError('At least one entry point has to be provided.')
|
290 |
+
|
291 |
+
video_count = 0
|
292 |
+
sip_count = 0
|
293 |
+
more_count = 0
|
294 |
+
for ep in self.entry_points:
|
295 |
+
if ep.entry_point_type == EntryPoint.VIDEO:
|
296 |
+
video_count += 1
|
297 |
+
elif ep.entry_point_type == EntryPoint.SIP:
|
298 |
+
sip_count += 1
|
299 |
+
elif ep.entry_point_type == EntryPoint.MORE:
|
300 |
+
more_count += 1
|
301 |
+
|
302 |
+
if video_count > 1:
|
303 |
+
raise ValueError('A conference can have zero or one `VIDEO` entry point.')
|
304 |
+
if sip_count > 1:
|
305 |
+
raise ValueError('A conference can have zero or one `SIP` entry point.')
|
306 |
+
if more_count > 1:
|
307 |
+
raise ValueError('A conference can have zero or one `MORE` entry point.')
|
308 |
+
if more_count == len(self.entry_points):
|
309 |
+
raise ValueError('A conference with only a `MORE` entry point is not a valid conference.')
|
310 |
+
|
311 |
+
def __eq__(self, other):
|
312 |
+
if not isinstance(other, ConferenceSolution):
|
313 |
+
return NotImplemented
|
314 |
+
elif self is other:
|
315 |
+
return True
|
316 |
+
else:
|
317 |
+
return (
|
318 |
+
super().__eq__(other)
|
319 |
+
and self.entry_points == other.entry_points
|
320 |
+
and self.solution_type == other.solution_type
|
321 |
+
and self.name == other.name
|
322 |
+
and self.icon_uri == other.icon_uri
|
323 |
+
)
|
324 |
+
|
325 |
+
def __str__(self):
|
326 |
+
return '{} - {}'.format(self.solution_type, self.entry_points)
|
327 |
+
|
328 |
+
def __repr__(self):
|
329 |
+
return '<ConferenceSolution {}>'.format(self.__str__())
|
330 |
+
|
331 |
+
|
332 |
+
class ConferenceSolutionCreateRequest(_BaseConferenceSolution):
|
333 |
+
"""
|
334 |
+
A request to generate a new conference and attach it to the event.
|
335 |
+
The data is generated asynchronously. To see whether the data is present check the status field.
|
336 |
+
"""
|
337 |
+
|
338 |
+
def __init__(
|
339 |
+
self,
|
340 |
+
solution_type: str = None,
|
341 |
+
request_id: str = None,
|
342 |
+
_status: str = None,
|
343 |
+
conference_id: str = None,
|
344 |
+
signature: str = None,
|
345 |
+
notes: str = None
|
346 |
+
):
|
347 |
+
"""
|
348 |
+
:param solution_type:
|
349 |
+
Solution type. See :py:class:`~gcsa.conference.SolutionType`
|
350 |
+
|
351 |
+
The possible values are:
|
352 |
+
|
353 |
+
* HANGOUT - for Hangouts for consumers (hangouts.google.com)
|
354 |
+
* NAMED_HANGOUT - for classic Hangouts for Google Workspace users (hangouts.google.com)
|
355 |
+
* HANGOUTS_MEET - for Google Meet (meet.google.com)
|
356 |
+
* ADD_ON - for 3P conference providers
|
357 |
+
|
358 |
+
:param request_id:
|
359 |
+
The client-generated unique ID for this request.
|
360 |
+
By default it is generated as UUID.
|
361 |
+
If you specify request_id manually, they should be unique for every new CreateRequest,
|
362 |
+
otherwise request will be ignored.
|
363 |
+
|
364 |
+
:param _status:
|
365 |
+
The current status of the conference create request. Should not be set by developer.
|
366 |
+
|
367 |
+
The possible values are:
|
368 |
+
|
369 |
+
* "pending": the conference create request is still being processed.
|
370 |
+
* "failure": the conference create request failed, there are no entry points.
|
371 |
+
* "success": the conference create request succeeded, the entry points are populated.
|
372 |
+
In this case `ConferenceSolution` with created entry points
|
373 |
+
is stored in the event's `conference_data`. And `ConferenceSolutionCreateRequest` is omitted.
|
374 |
+
:param conference_id:
|
375 |
+
The ID of the conference. Optional.
|
376 |
+
Can be used by developers to keep track of conferences, should not be displayed to users.
|
377 |
+
|
378 |
+
Values for solution types (see :py:class:`~gcsa.conference.SolutionType`):
|
379 |
+
|
380 |
+
* HANGOUT: unset
|
381 |
+
* NAMED_HANGOUT: the name of the Hangout
|
382 |
+
* HANGOUTS_MEET: the 10-letter meeting code, for example "aaa-bbbb-ccc"
|
383 |
+
* ADD_ON: defined by 3P conference provider
|
384 |
+
|
385 |
+
:param signature:
|
386 |
+
The signature of the conference data.
|
387 |
+
Generated on server side. Must be preserved while copying the conference data between events,
|
388 |
+
otherwise the conference data will not be copied.
|
389 |
+
None for a conference with a failed create request.
|
390 |
+
Optional for a conference with a pending create request.
|
391 |
+
:param notes:
|
392 |
+
String of additional notes (such as instructions from the domain administrator, legal notices)
|
393 |
+
to display to the user. Can contain HTML. The maximum length is 2048 characters
|
394 |
+
"""
|
395 |
+
super().__init__(conference_id=conference_id, signature=signature, notes=notes, _status=_status)
|
396 |
+
self.request_id = request_id or uuid4().hex
|
397 |
+
self.solution_type = solution_type
|
398 |
+
|
399 |
+
def __eq__(self, other):
|
400 |
+
if not isinstance(other, ConferenceSolutionCreateRequest):
|
401 |
+
return NotImplemented
|
402 |
+
elif self is other:
|
403 |
+
return True
|
404 |
+
else:
|
405 |
+
return (
|
406 |
+
super().__eq__(other)
|
407 |
+
and self.request_id == other.request_id
|
408 |
+
and self.solution_type == other.solution_type
|
409 |
+
and self.status == other.status
|
410 |
+
)
|
411 |
+
|
412 |
+
def __str__(self):
|
413 |
+
return "{} - status:'{}'".format(self.solution_type, self.status)
|
414 |
+
|
415 |
+
def __repr__(self):
|
416 |
+
return '<ConferenceSolutionCreateRequest {}>'.format(self.__str__())
|
google-calendar-simple-api/build/lib/gcsa/event.py
ADDED
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import total_ordering
|
2 |
+
from typing import List, Union
|
3 |
+
|
4 |
+
from beautiful_date import BeautifulDate
|
5 |
+
from tzlocal import get_localzone_name
|
6 |
+
from datetime import datetime, date, timedelta, time
|
7 |
+
|
8 |
+
from ._resource import Resource
|
9 |
+
from .attachment import Attachment
|
10 |
+
from .attendee import Attendee
|
11 |
+
from .conference import ConferenceSolution, ConferenceSolutionCreateRequest
|
12 |
+
from .person import Person
|
13 |
+
from .reminders import PopupReminder, EmailReminder, Reminder
|
14 |
+
from .util.date_time_util import ensure_localisation
|
15 |
+
|
16 |
+
|
17 |
+
class Visibility:
|
18 |
+
"""Possible values of the event visibility.
|
19 |
+
|
20 |
+
* `DEFAULT` - Uses the default visibility for events on the calendar. This is the default value.
|
21 |
+
* `PUBLIC` - The event is public and event details are visible to all readers of the calendar.
|
22 |
+
* `PRIVATE` - The event is private and only event attendees may view event details.
|
23 |
+
"""
|
24 |
+
|
25 |
+
DEFAULT = "default"
|
26 |
+
PUBLIC = "public"
|
27 |
+
PRIVATE = "private"
|
28 |
+
|
29 |
+
|
30 |
+
class Transparency:
|
31 |
+
"""Possible values of the event transparency.
|
32 |
+
|
33 |
+
* `OPAQUE` - Default value. The event does block time on the calendar.
|
34 |
+
This is equivalent to setting 'Show me as' to 'Busy' in the Calendar UI.
|
35 |
+
* `TRANSPARENT` - The event does not block time on the calendar.
|
36 |
+
This is equivalent to setting 'Show me as' to 'Available' in the Calendar UI.
|
37 |
+
"""
|
38 |
+
|
39 |
+
OPAQUE = 'opaque'
|
40 |
+
TRANSPARENT = 'transparent'
|
41 |
+
|
42 |
+
|
43 |
+
@total_ordering
|
44 |
+
class Event(Resource):
|
45 |
+
def __init__(
|
46 |
+
self,
|
47 |
+
summary: str,
|
48 |
+
start: Union[date, datetime, BeautifulDate],
|
49 |
+
end: Union[date, datetime, BeautifulDate] = None,
|
50 |
+
*,
|
51 |
+
timezone: str = get_localzone_name(),
|
52 |
+
event_id: str = None,
|
53 |
+
description: str = None,
|
54 |
+
location: str = None,
|
55 |
+
recurrence: Union[str, List[str]] = None,
|
56 |
+
color_id: str = None,
|
57 |
+
visibility: str = Visibility.DEFAULT,
|
58 |
+
attendees: Union[Attendee, str, List[Attendee], List[str]] = None,
|
59 |
+
attachments: Union[Attachment, List[Attachment]] = None,
|
60 |
+
conference_solution: Union[ConferenceSolution, ConferenceSolutionCreateRequest] = None,
|
61 |
+
reminders: Union[Reminder, List[Reminder]] = None,
|
62 |
+
default_reminders: bool = False,
|
63 |
+
minutes_before_popup_reminder: int = None,
|
64 |
+
minutes_before_email_reminder: int = None,
|
65 |
+
guests_can_invite_others: bool = True,
|
66 |
+
guests_can_modify: bool = False,
|
67 |
+
guests_can_see_other_guests: bool = True,
|
68 |
+
transparency: str = None,
|
69 |
+
_creator: Person = None,
|
70 |
+
_organizer: Person = None,
|
71 |
+
_created: datetime = None,
|
72 |
+
_updated: datetime = None,
|
73 |
+
_recurring_event_id: str = None,
|
74 |
+
**other
|
75 |
+
):
|
76 |
+
"""
|
77 |
+
:param summary:
|
78 |
+
Title of the event.
|
79 |
+
:param start:
|
80 |
+
Starting date/datetime.
|
81 |
+
:param end:
|
82 |
+
Ending date/datetime. If 'end' is not specified, event is considered as a 1-day or 1-hour event
|
83 |
+
if 'start' is date or datetime respectively.
|
84 |
+
:param timezone:
|
85 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
86 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
87 |
+
:param event_id:
|
88 |
+
Opaque identifier of the event. By default, it is generated by the server. You can specify id as a
|
89 |
+
5-1024 long string of characters used in base32hex ([a-vA-V0-9]). The ID must be unique per
|
90 |
+
calendar.
|
91 |
+
:param description:
|
92 |
+
Description of the event. Can contain HTML.
|
93 |
+
:param location:
|
94 |
+
Geographic location of the event as free-form text.
|
95 |
+
:param recurrence:
|
96 |
+
RRULE/RDATE/EXRULE/EXDATE string or list of such strings. See :py:mod:`~gcsa.recurrence`
|
97 |
+
:param color_id:
|
98 |
+
Color id referring to an entry from colors endpoint.
|
99 |
+
See :py:meth:`~gcsa.google_calendar.GoogleCalendar.list_event_colors`
|
100 |
+
:param visibility:
|
101 |
+
Visibility of the event. Default is default visibility for events on the calendar.
|
102 |
+
See :py:class:`~gcsa.event.Visibility`
|
103 |
+
:param attendees:
|
104 |
+
Attendee or list of attendees. See :py:class:`~gcsa.attendee.Attendee`.
|
105 |
+
Each attendee may be given as email string or :py:class:`~gcsa.attendee.Attendee` object.
|
106 |
+
:param attachments:
|
107 |
+
Attachment or list of attachments. See :py:class:`~gcsa.attachment.Attachment`
|
108 |
+
:param conference_solution:
|
109 |
+
:py:class:`~gcsa.conference.ConferenceSolutionCreateRequest` object to create a new conference
|
110 |
+
or :py:class:`~gcsa.conference.ConferenceSolution` object for existing conference.
|
111 |
+
:param reminders:
|
112 |
+
Reminder or list of reminder objects. See :py:mod:`~gcsa.reminders`
|
113 |
+
:param default_reminders:
|
114 |
+
Whether the default reminders of the calendar apply to the event.
|
115 |
+
:param minutes_before_popup_reminder:
|
116 |
+
Minutes before popup reminder or None if reminder is not needed.
|
117 |
+
:param minutes_before_email_reminder:
|
118 |
+
Minutes before email reminder or None if reminder is not needed.
|
119 |
+
:param guests_can_invite_others:
|
120 |
+
Whether attendees other than the organizer can invite others to the event.
|
121 |
+
:param guests_can_modify:
|
122 |
+
Whether attendees other than the organizer can modify the event.
|
123 |
+
:param guests_can_see_other_guests:
|
124 |
+
Whether attendees other than the organizer can see who the event's attendees are.
|
125 |
+
:param transparency:
|
126 |
+
Whether the event blocks time on the calendar. See :py:class:`~gcsa.event.Transparency`
|
127 |
+
:param _creator:
|
128 |
+
The creator of the event. See :py:class:`~gcsa.person.Person`
|
129 |
+
:param _organizer:
|
130 |
+
The organizer of the event. See :py:class:`~gcsa.person.Person`.
|
131 |
+
If the organizer is also an attendee, this is indicated with a separate entry in attendees with
|
132 |
+
the organizer field set to True.
|
133 |
+
To change the organizer, use the move operation
|
134 |
+
see :py:meth:`~gcsa.google_calendar.GoogleCalendar.move_event`
|
135 |
+
:param _created:
|
136 |
+
Creation time of the event. Read-only.
|
137 |
+
:param _updated:
|
138 |
+
Last modification time of the event. Read-only.
|
139 |
+
:param _recurring_event_id:
|
140 |
+
For an instance of a recurring event, this is the id of the recurring event to which
|
141 |
+
this instance belongs. Read-only.
|
142 |
+
:param other:
|
143 |
+
Other fields that should be included in request json. Will be included as they are.
|
144 |
+
See more in https://developers.google.com/calendar/v3/reference/events
|
145 |
+
"""
|
146 |
+
|
147 |
+
def ensure_list(obj):
|
148 |
+
return [] if obj is None else obj if isinstance(obj, list) else [obj]
|
149 |
+
|
150 |
+
self.timezone = timezone
|
151 |
+
self.start = start
|
152 |
+
if end or start is None:
|
153 |
+
self.end = end
|
154 |
+
elif isinstance(start, datetime):
|
155 |
+
self.end = start + timedelta(hours=1)
|
156 |
+
elif isinstance(start, date):
|
157 |
+
self.end = start + timedelta(days=1)
|
158 |
+
|
159 |
+
if isinstance(self.start, datetime) and isinstance(self.end, datetime):
|
160 |
+
self.start = ensure_localisation(self.start, timezone)
|
161 |
+
self.end = ensure_localisation(self.end, timezone)
|
162 |
+
elif isinstance(self.start, datetime) or isinstance(self.end, datetime):
|
163 |
+
raise TypeError('Start and end must either both be date or both be datetime.')
|
164 |
+
|
165 |
+
def ensure_date(d):
|
166 |
+
"""Converts d to date if it is of type BeautifulDate."""
|
167 |
+
if isinstance(d, BeautifulDate):
|
168 |
+
return date(year=d.year, month=d.month, day=d.day)
|
169 |
+
else:
|
170 |
+
return d
|
171 |
+
|
172 |
+
self.start = ensure_date(self.start)
|
173 |
+
self.end = ensure_date(self.end)
|
174 |
+
|
175 |
+
self.created = _created
|
176 |
+
self.updated = _updated
|
177 |
+
|
178 |
+
attendees = [self._ensure_attendee_from_email(a) for a in ensure_list(attendees)]
|
179 |
+
reminders = ensure_list(reminders)
|
180 |
+
|
181 |
+
if len(reminders) > 5:
|
182 |
+
raise ValueError('The maximum number of override reminders is 5.')
|
183 |
+
|
184 |
+
if default_reminders and reminders:
|
185 |
+
raise ValueError('Cannot specify both default reminders and overrides at the same time.')
|
186 |
+
|
187 |
+
self.event_id = event_id
|
188 |
+
self.summary = summary
|
189 |
+
self.description = description
|
190 |
+
self.location = location
|
191 |
+
self.recurrence = ensure_list(recurrence)
|
192 |
+
self.color_id = color_id
|
193 |
+
self.visibility = visibility
|
194 |
+
self.attendees = attendees
|
195 |
+
self.attachments = ensure_list(attachments)
|
196 |
+
self.conference_solution = conference_solution
|
197 |
+
self.reminders = reminders
|
198 |
+
self.default_reminders = default_reminders
|
199 |
+
self.recurring_event_id = _recurring_event_id
|
200 |
+
self.guests_can_invite_others = guests_can_invite_others
|
201 |
+
self.guests_can_modify = guests_can_modify
|
202 |
+
self.guests_can_see_other_guests = guests_can_see_other_guests
|
203 |
+
self.transparency = transparency
|
204 |
+
self.creator = _creator
|
205 |
+
self.organizer = _organizer
|
206 |
+
|
207 |
+
self.other = other
|
208 |
+
|
209 |
+
if minutes_before_popup_reminder is not None:
|
210 |
+
self.add_popup_reminder(minutes_before_popup_reminder)
|
211 |
+
if minutes_before_email_reminder is not None:
|
212 |
+
self.add_email_reminder(minutes_before_email_reminder)
|
213 |
+
|
214 |
+
@property
|
215 |
+
def id(self):
|
216 |
+
return self.event_id
|
217 |
+
|
218 |
+
def add_attendee(
|
219 |
+
self,
|
220 |
+
attendee: Union[str, Attendee]
|
221 |
+
):
|
222 |
+
"""Adds attendee to an event. See :py:class:`~gcsa.attendee.Attendee`.
|
223 |
+
Attendee may be given as email string or :py:class:`~gcsa.attendee.Attendee` object."""
|
224 |
+
self.attendees.append(self._ensure_attendee_from_email(attendee))
|
225 |
+
|
226 |
+
def add_attendees(
|
227 |
+
self,
|
228 |
+
attendees: List[Union[str, Attendee]]
|
229 |
+
):
|
230 |
+
"""Adds multiple attendees to an event. See :py:class:`~gcsa.attendee.Attendee`.
|
231 |
+
Each attendee may be given as email string or :py:class:`~gcsa.attendee.Attendee` object."""
|
232 |
+
for a in attendees:
|
233 |
+
self.add_attendee(a)
|
234 |
+
|
235 |
+
def add_attachment(
|
236 |
+
self,
|
237 |
+
file_url: str,
|
238 |
+
title: str = None,
|
239 |
+
mime_type: str = None
|
240 |
+
):
|
241 |
+
"""Adds attachment to an event. See :py:class:`~gcsa.attachment.Attachment`"""
|
242 |
+
self.attachments.append(Attachment(file_url=file_url, title=title, mime_type=mime_type))
|
243 |
+
|
244 |
+
def add_email_reminder(
|
245 |
+
self,
|
246 |
+
minutes_before_start: int = None,
|
247 |
+
days_before: int = None,
|
248 |
+
at: time = None
|
249 |
+
):
|
250 |
+
"""Adds email reminder to an event. See :py:class:`~gcsa.reminders.EmailReminder`"""
|
251 |
+
self.add_reminder(EmailReminder(minutes_before_start, days_before, at))
|
252 |
+
|
253 |
+
def add_popup_reminder(
|
254 |
+
self,
|
255 |
+
minutes_before_start: int = None,
|
256 |
+
days_before: int = None,
|
257 |
+
at: time = None
|
258 |
+
):
|
259 |
+
"""Adds popup reminder to an event. See :py:class:`~gcsa.reminders.PopupReminder`"""
|
260 |
+
self.add_reminder(PopupReminder(minutes_before_start, days_before, at))
|
261 |
+
|
262 |
+
def add_reminder(
|
263 |
+
self,
|
264 |
+
reminder: Reminder
|
265 |
+
):
|
266 |
+
"""Adds reminder to an event. See :py:mod:`~gcsa.reminders`"""
|
267 |
+
if len(self.reminders) > 4:
|
268 |
+
raise ValueError('The maximum number of override reminders is 5.')
|
269 |
+
self.reminders.append(reminder)
|
270 |
+
|
271 |
+
@staticmethod
|
272 |
+
def _ensure_attendee_from_email(
|
273 |
+
attendee_or_email: Union[str, Attendee]
|
274 |
+
):
|
275 |
+
"""If attendee_or_email is email string, returns created :py:class:`~gcsa.attendee.Attendee`
|
276 |
+
object with the given email."""
|
277 |
+
if isinstance(attendee_or_email, str):
|
278 |
+
return Attendee(email=attendee_or_email)
|
279 |
+
else:
|
280 |
+
return attendee_or_email
|
281 |
+
|
282 |
+
@property
|
283 |
+
def is_recurring_instance(self):
|
284 |
+
return self.recurring_event_id is not None
|
285 |
+
|
286 |
+
def __str__(self):
|
287 |
+
return '{} - {}'.format(self.start, self.summary)
|
288 |
+
|
289 |
+
def __repr__(self):
|
290 |
+
return '<Event {}>'.format(self.__str__())
|
291 |
+
|
292 |
+
def __lt__(self, other):
|
293 |
+
def ensure_datetime(d, timezone):
|
294 |
+
if type(d) is date:
|
295 |
+
return ensure_localisation(datetime(year=d.year, month=d.month, day=d.day), timezone)
|
296 |
+
else:
|
297 |
+
return d
|
298 |
+
|
299 |
+
start = ensure_datetime(self.start, self.timezone)
|
300 |
+
end = ensure_datetime(self.end, self.timezone)
|
301 |
+
|
302 |
+
other_start = ensure_datetime(other.start, other.timezone)
|
303 |
+
other_end = ensure_datetime(other.end, other.timezone)
|
304 |
+
|
305 |
+
return (start, end) < (other_start, other_end)
|
306 |
+
|
307 |
+
def __eq__(self, other):
|
308 |
+
return (
|
309 |
+
isinstance(other, Event)
|
310 |
+
and self.start == other.start
|
311 |
+
and self.end == other.end
|
312 |
+
and self.event_id == other.event_id
|
313 |
+
and self.summary == other.summary
|
314 |
+
and self.description == other.description
|
315 |
+
and self.location == other.location
|
316 |
+
and self.recurrence == other.recurrence
|
317 |
+
and self.color_id == other.color_id
|
318 |
+
and self.visibility == other.visibility
|
319 |
+
and self.attendees == other.attendees
|
320 |
+
and self.attachments == other.attachments
|
321 |
+
and self.reminders == other.reminders
|
322 |
+
and self.default_reminders == other.default_reminders
|
323 |
+
and self.created == other.created
|
324 |
+
and self.updated == other.updated
|
325 |
+
and self.recurring_event_id == other.recurring_event_id
|
326 |
+
and self.guests_can_invite_others == other.guests_can_invite_others
|
327 |
+
and self.guests_can_modify == other.guests_can_modify
|
328 |
+
and self.guests_can_see_other_guests == other.guests_can_see_other_guests
|
329 |
+
and self.other == other.other
|
330 |
+
)
|
google-calendar-simple-api/build/lib/gcsa/free_busy.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from collections import namedtuple
|
3 |
+
from datetime import datetime
|
4 |
+
from typing import Dict, List
|
5 |
+
|
6 |
+
TimeRange = namedtuple('TimeRange', ('start', 'end'))
|
7 |
+
|
8 |
+
|
9 |
+
class FreeBusy:
|
10 |
+
def __init__(
|
11 |
+
self,
|
12 |
+
*,
|
13 |
+
time_min: datetime,
|
14 |
+
time_max: datetime,
|
15 |
+
groups: Dict[str, List[str]],
|
16 |
+
calendars: Dict[str, List[TimeRange]],
|
17 |
+
groups_errors: Dict = None,
|
18 |
+
calendars_errors: Dict = None,
|
19 |
+
):
|
20 |
+
"""Represents free/busy information for a given calendar(s) and/or group(s)
|
21 |
+
|
22 |
+
:param time_min:
|
23 |
+
The start of the interval.
|
24 |
+
:param time_max:
|
25 |
+
The end of the interval.
|
26 |
+
:param groups:
|
27 |
+
Expansion of groups.
|
28 |
+
Dictionary that maps the name of the group to the list of calendars that are members of this group.
|
29 |
+
:param calendars:
|
30 |
+
Free/busy information for calendars.
|
31 |
+
Dictionary that maps calendar id to the list of time ranges during which this calendar should be
|
32 |
+
regarded as busy.
|
33 |
+
:param groups_errors:
|
34 |
+
Optional error(s) (if computation for the group failed).
|
35 |
+
Dictionary that maps the name of the group to the list of errors.
|
36 |
+
:param calendars_errors:
|
37 |
+
Optional error(s) (if computation for the calendar failed).
|
38 |
+
Dictionary that maps calendar id to the list of errors.
|
39 |
+
|
40 |
+
|
41 |
+
.. note:: Errors have the following format:
|
42 |
+
|
43 |
+
.. code-block::
|
44 |
+
|
45 |
+
{
|
46 |
+
"domain": "<domain>",
|
47 |
+
"reason": "<reason>"
|
48 |
+
}
|
49 |
+
|
50 |
+
Some possible values for "reason" are:
|
51 |
+
|
52 |
+
* "groupTooBig" - The group of users requested is too large for a single query.
|
53 |
+
* "tooManyCalendarsRequested" - The number of calendars requested is too large for a single query.
|
54 |
+
* "notFound" - The requested resource was not found.
|
55 |
+
* "internalError" - The API service has encountered an internal error.
|
56 |
+
|
57 |
+
Additional error types may be added in the future.
|
58 |
+
"""
|
59 |
+
self.time_min = time_min
|
60 |
+
self.time_max = time_max
|
61 |
+
self.groups = groups
|
62 |
+
self.calendars = calendars
|
63 |
+
self.groups_errors = groups_errors or {}
|
64 |
+
self.calendars_errors = calendars_errors or {}
|
65 |
+
|
66 |
+
def __iter__(self):
|
67 |
+
"""
|
68 |
+
:returns:
|
69 |
+
list of 'TimeRange's during which this calendar should be regarded as busy.
|
70 |
+
:raises:
|
71 |
+
ValueError if requested all requested calendars have errors
|
72 |
+
or more than one calendar has been requested.
|
73 |
+
"""
|
74 |
+
if len(self.calendars) == 0:
|
75 |
+
raise ValueError("No free/busy information has been received. "
|
76 |
+
"Check the 'calendars_errors' and 'groups_errors' fields.")
|
77 |
+
if len(self.calendars) > 1 or len(self.calendars_errors) > 0:
|
78 |
+
raise ValueError("Can't iterate over FreeBusy objects directly when more than one calendars were requested."
|
79 |
+
"Use 'calendars' field instead to get free/busy information of the specific calendar.")
|
80 |
+
return iter(next(iter(self.calendars.values())))
|
81 |
+
|
82 |
+
def __str__(self):
|
83 |
+
return '<FreeBusy {} - {}>'.format(self.time_min, self.time_max)
|
84 |
+
|
85 |
+
def __repr__(self):
|
86 |
+
return self.__str__()
|
87 |
+
|
88 |
+
|
89 |
+
class FreeBusyQueryError(Exception):
|
90 |
+
def __init__(self, groups_errors, calendars_errors):
|
91 |
+
message = '\n'
|
92 |
+
if groups_errors:
|
93 |
+
message += f'Groups errors: {json.dumps(groups_errors, indent=4)}'
|
94 |
+
if calendars_errors:
|
95 |
+
message += f'Calendars errors: {json.dumps(calendars_errors, indent=4)}'
|
96 |
+
super().__init__(message)
|
97 |
+
self.groups_errors = groups_errors
|
98 |
+
self.calendars_errors = calendars_errors
|
google-calendar-simple-api/build/lib/gcsa/google_calendar.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from google.oauth2.credentials import Credentials
|
2 |
+
|
3 |
+
from ._services.acl_service import ACLService
|
4 |
+
from ._services.events_service import EventsService, SendUpdatesMode # noqa: F401
|
5 |
+
from ._services.calendars_service import CalendarsService
|
6 |
+
from ._services.calendar_lists_service import CalendarListService
|
7 |
+
from ._services.colors_service import ColorsService
|
8 |
+
from ._services.free_busy_service import FreeBusyService
|
9 |
+
from ._services.settings_service import SettingsService
|
10 |
+
|
11 |
+
|
12 |
+
class GoogleCalendar(
|
13 |
+
EventsService,
|
14 |
+
CalendarsService,
|
15 |
+
CalendarListService,
|
16 |
+
ColorsService,
|
17 |
+
SettingsService,
|
18 |
+
ACLService,
|
19 |
+
FreeBusyService
|
20 |
+
):
|
21 |
+
"""Collection of all supported methods for events and calendars management."""
|
22 |
+
|
23 |
+
def __init__(
|
24 |
+
self,
|
25 |
+
default_calendar: str = 'primary',
|
26 |
+
*,
|
27 |
+
credentials: Credentials = None,
|
28 |
+
credentials_path: str = None,
|
29 |
+
token_path: str = None,
|
30 |
+
save_token: bool = True,
|
31 |
+
read_only: bool = False,
|
32 |
+
authentication_flow_host: str = 'localhost',
|
33 |
+
authentication_flow_port: int = 8080,
|
34 |
+
authentication_flow_bind_addr: str = None
|
35 |
+
):
|
36 |
+
"""
|
37 |
+
Specify ``credentials`` to use in requests or ``credentials_path`` and ``token_path`` to get credentials from.
|
38 |
+
|
39 |
+
:param default_calendar:
|
40 |
+
Users email address or name/id of the calendar. Default: primary calendar of the user
|
41 |
+
|
42 |
+
If user's email or "primary" is specified, then primary calendar of the user is used.
|
43 |
+
You don't need to specify this parameter in this case as it is a default behaviour.
|
44 |
+
|
45 |
+
To use a different calendar you need to specify its id.
|
46 |
+
Go to calendar's `settings and sharing` -> `Integrate calendar` -> `Calendar ID`.
|
47 |
+
:param credentials:
|
48 |
+
Credentials with token and refresh token.
|
49 |
+
If specified, ``credentials_path``, ``token_path``, and ``save_token`` are ignored.
|
50 |
+
If not specified, credentials are retrieved from "token.pickle" file (specified in ``token_path`` or
|
51 |
+
default path) or with authentication flow using secret from "credentials.json" ("client_secret_*.json")
|
52 |
+
(specified in ``credentials_path`` or default path)
|
53 |
+
:param credentials_path:
|
54 |
+
Path to "credentials.json" ("client_secret_*.json") file.
|
55 |
+
Default: ~/.credentials/credentials.json or ~/.credentials/client_secret*.json
|
56 |
+
:param token_path:
|
57 |
+
Existing path to load the token from, or path to save the token after initial authentication flow.
|
58 |
+
Default: "token.pickle" in the same directory as the credentials_path
|
59 |
+
:param save_token:
|
60 |
+
Whether to pickle token after authentication flow for future uses
|
61 |
+
:param read_only:
|
62 |
+
If require read only access. Default: False
|
63 |
+
:param authentication_flow_host:
|
64 |
+
Host to receive response during authentication flow
|
65 |
+
:param authentication_flow_port:
|
66 |
+
Port to receive response during authentication flow
|
67 |
+
:param authentication_flow_bind_addr:
|
68 |
+
Optional IP address for the redirect server to listen on when it is not the same as host
|
69 |
+
(e.g. in a container)
|
70 |
+
"""
|
71 |
+
super().__init__(
|
72 |
+
default_calendar=default_calendar,
|
73 |
+
credentials=credentials,
|
74 |
+
credentials_path=credentials_path,
|
75 |
+
token_path=token_path,
|
76 |
+
save_token=save_token,
|
77 |
+
read_only=read_only,
|
78 |
+
authentication_flow_host=authentication_flow_host,
|
79 |
+
authentication_flow_port=authentication_flow_port,
|
80 |
+
authentication_flow_bind_addr=authentication_flow_bind_addr
|
81 |
+
)
|
google-calendar-simple-api/build/lib/gcsa/person.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Person:
|
2 |
+
def __init__(
|
3 |
+
self,
|
4 |
+
email: str = None,
|
5 |
+
display_name: str = None,
|
6 |
+
_id: str = None,
|
7 |
+
_is_self: bool = None
|
8 |
+
):
|
9 |
+
"""Represents organizer's, creator's, or primary attendee's fields.
|
10 |
+
For attendees see more in :py:class:`~gcsa.attendee.Attendee`.
|
11 |
+
|
12 |
+
:param email:
|
13 |
+
The person's email address, if available
|
14 |
+
:param display_name:
|
15 |
+
The person's name, if available
|
16 |
+
:param _id:
|
17 |
+
The person's Profile ID, if available.
|
18 |
+
It corresponds to the id field in the People collection of the Google+ API
|
19 |
+
:param _is_self:
|
20 |
+
Whether the person corresponds to the calendar on which the copy of the event appears.
|
21 |
+
The default is False (set by Google's API).
|
22 |
+
"""
|
23 |
+
self.email = email
|
24 |
+
self.display_name = display_name
|
25 |
+
self.id_ = _id
|
26 |
+
self.is_self = _is_self
|
27 |
+
|
28 |
+
def __eq__(self, other):
|
29 |
+
return (
|
30 |
+
isinstance(other, Person)
|
31 |
+
and self.email == other.email
|
32 |
+
and self.display_name == other.display_name
|
33 |
+
and self.id_ == other.id_
|
34 |
+
and self.is_self == other.is_self
|
35 |
+
)
|
36 |
+
|
37 |
+
def __str__(self):
|
38 |
+
return "'{}' - '{}'".format(self.email, self.display_name)
|
39 |
+
|
40 |
+
def __repr__(self):
|
41 |
+
return '<Person {}>'.format(self.__str__())
|
google-calendar-simple-api/build/lib/gcsa/recurrence.py
ADDED
@@ -0,0 +1,570 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, date
|
2 |
+
|
3 |
+
from tzlocal import get_localzone_name
|
4 |
+
|
5 |
+
from .util.date_time_util import ensure_localisation
|
6 |
+
|
7 |
+
|
8 |
+
class Duration:
|
9 |
+
"""Represents properties that contain a duration of time."""
|
10 |
+
|
11 |
+
def __init__(self, w=None, d=None, h=None, m=None, s=None):
|
12 |
+
"""
|
13 |
+
:param w: weeks
|
14 |
+
:param d: days
|
15 |
+
:param h: hours
|
16 |
+
:param m: minutes
|
17 |
+
:param s: seconds
|
18 |
+
"""
|
19 |
+
|
20 |
+
self.w = w
|
21 |
+
self.d = d
|
22 |
+
self.h = h
|
23 |
+
self.m = m
|
24 |
+
self.s = s
|
25 |
+
|
26 |
+
def __str__(self):
|
27 |
+
res = 'P'
|
28 |
+
if self.w:
|
29 |
+
res += '{}W'.format(self.w)
|
30 |
+
if self.d:
|
31 |
+
res += '{}D'.format(self.d)
|
32 |
+
if self.h or self.m or self.s:
|
33 |
+
res += 'T'
|
34 |
+
if self.h:
|
35 |
+
res += '{}H'.format(self.h)
|
36 |
+
if self.m:
|
37 |
+
res += '{}M'.format(self.m)
|
38 |
+
if self.s:
|
39 |
+
res += '{}S'.format(self.s)
|
40 |
+
|
41 |
+
return res
|
42 |
+
|
43 |
+
|
44 |
+
class _DayOfTheWeek:
|
45 |
+
"""Weekday representation. Optionally includes positive or negative integer
|
46 |
+
value that indicates the nth occurrence of a specific day within the "MONTHLY"
|
47 |
+
or "YEARLY" recurrence rules.
|
48 |
+
|
49 |
+
>>> str(SU)
|
50 |
+
'SU'
|
51 |
+
|
52 |
+
>>> str(FR)
|
53 |
+
'FR'
|
54 |
+
|
55 |
+
>>> str(SU(4))
|
56 |
+
'4SU'
|
57 |
+
|
58 |
+
>>> str(SU(-1))
|
59 |
+
'-1SU'
|
60 |
+
"""
|
61 |
+
|
62 |
+
def __init__(self, short, n=None):
|
63 |
+
self.short = short
|
64 |
+
self.n = n
|
65 |
+
|
66 |
+
def __call__(self, n):
|
67 |
+
return _DayOfTheWeek(self.short, n)
|
68 |
+
|
69 |
+
def __str__(self):
|
70 |
+
if self.n is None:
|
71 |
+
return self.short
|
72 |
+
else:
|
73 |
+
return str(self.n) + self.short
|
74 |
+
|
75 |
+
|
76 |
+
SU = SUNDAY = _DayOfTheWeek('SU')
|
77 |
+
MO = MONDAY = _DayOfTheWeek('MO')
|
78 |
+
TU = TUESDAY = _DayOfTheWeek('TU')
|
79 |
+
WE = WEDNESDAY = _DayOfTheWeek('WE')
|
80 |
+
TH = THURSDAY = _DayOfTheWeek('TH')
|
81 |
+
FR = FRIDAY = _DayOfTheWeek('FR')
|
82 |
+
SA = SATURDAY = _DayOfTheWeek('SA')
|
83 |
+
|
84 |
+
DEFAULT_WEEK_START = SUNDAY
|
85 |
+
|
86 |
+
SECONDLY = 'SECONDLY'
|
87 |
+
MINUTELY = 'MINUTELY'
|
88 |
+
HOURLY = 'HOURLY'
|
89 |
+
|
90 |
+
DAILY = 'DAILY'
|
91 |
+
WEEKLY = 'WEEKLY'
|
92 |
+
MONTHLY = 'MONTHLY'
|
93 |
+
YEARLY = 'YEARLY'
|
94 |
+
|
95 |
+
|
96 |
+
class Recurrence:
|
97 |
+
|
98 |
+
@staticmethod
|
99 |
+
def rule(
|
100 |
+
freq=DAILY,
|
101 |
+
interval=None,
|
102 |
+
count=None,
|
103 |
+
until=None,
|
104 |
+
by_second=None,
|
105 |
+
by_minute=None,
|
106 |
+
by_hour=None,
|
107 |
+
by_week_day=None,
|
108 |
+
by_month_day=None,
|
109 |
+
by_year_day=None,
|
110 |
+
by_week=None,
|
111 |
+
by_month=None,
|
112 |
+
by_set_pos=None,
|
113 |
+
week_start=DEFAULT_WEEK_START
|
114 |
+
):
|
115 |
+
"""This property defines a rule or repeating pattern for recurring events.
|
116 |
+
|
117 |
+
:param freq:
|
118 |
+
Identifies the type of recurrence rule. Possible values are SECONDLY, HOURLY,
|
119 |
+
MINUTELY, DAILY, WEEKLY, MONTHLY, YEARLY. Default: DAILY
|
120 |
+
:param interval:
|
121 |
+
Positive integer representing how often the recurrence rule repeats
|
122 |
+
:param count:
|
123 |
+
Number of occurrences at which to range-bound the recurrence
|
124 |
+
:param until:
|
125 |
+
End date of recurrence
|
126 |
+
:param by_second:
|
127 |
+
Second or list of seconds within a minute. Valid values are 0 to 60
|
128 |
+
:param by_minute:
|
129 |
+
Minute or list of minutes within a hour. Valid values are 0 to 59
|
130 |
+
:param by_hour:
|
131 |
+
Hour or list of hours of the day. Valid values are 0 to 23
|
132 |
+
:param by_week_day:
|
133 |
+
Day or list of days of the week.
|
134 |
+
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
135 |
+
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
136 |
+
:param by_month_day:
|
137 |
+
Day or list of days of the month. Valid values are 1 to 31 or -31 to -1.
|
138 |
+
For example, -10 represents the tenth to the last day of the month.
|
139 |
+
:param by_year_day:
|
140 |
+
Day or list of days of the year. Valid values are 1 to 366 or -366 to -1.
|
141 |
+
For example, -1 represents the last day of the year.
|
142 |
+
:param by_week:
|
143 |
+
Ordinal or list of ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to -1.
|
144 |
+
:param by_month:
|
145 |
+
Month or list of months of the year. Valid values are 1 to 12.
|
146 |
+
:param by_set_pos:
|
147 |
+
Value or list of values which corresponds to the nth occurrence within the set of events
|
148 |
+
specified by the rule. Valid values are 1 to 366 or -366 to -1.
|
149 |
+
It can only be used in conjunction with another by_xxx parameter.
|
150 |
+
:param week_start:
|
151 |
+
The day on which the workweek starts.
|
152 |
+
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
153 |
+
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
154 |
+
|
155 |
+
:return:
|
156 |
+
String representing specified recurrence rule in `RRULE format`_.
|
157 |
+
|
158 |
+
.. note:: If none of the by_day, by_month_day, or by_year_day are specified, the day is gotten from start date.
|
159 |
+
|
160 |
+
|
161 |
+
.. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
|
162 |
+
"""
|
163 |
+
return 'RRULE:' + Recurrence._rule(freq, interval, count, until, by_second, by_minute, by_hour, by_week_day,
|
164 |
+
by_month_day, by_year_day, by_week, by_month, by_set_pos, week_start)
|
165 |
+
|
166 |
+
@staticmethod
|
167 |
+
def exclude_rule(
|
168 |
+
freq=DAILY,
|
169 |
+
interval=None,
|
170 |
+
count=None,
|
171 |
+
until=None,
|
172 |
+
by_second=None,
|
173 |
+
by_minute=None,
|
174 |
+
by_hour=None,
|
175 |
+
by_week_day=None,
|
176 |
+
by_month_day=None,
|
177 |
+
by_year_day=None,
|
178 |
+
by_week=None,
|
179 |
+
by_month=None,
|
180 |
+
by_set_pos=None,
|
181 |
+
week_start=DEFAULT_WEEK_START
|
182 |
+
):
|
183 |
+
"""This property defines an exclusion rule or repeating pattern for recurring events.
|
184 |
+
|
185 |
+
:param freq:
|
186 |
+
Identifies the type of recurrence rule. Possible values are SECONDLY, HOURLY,
|
187 |
+
MINUTELY, DAILY, WEEKLY, MONTHLY, YEARLY. Default: DAILY
|
188 |
+
:param interval:
|
189 |
+
Positive integer representing how often the recurrence rule repeats
|
190 |
+
:param count:
|
191 |
+
Number of occurrences at which to range-bound the recurrence
|
192 |
+
:param until:
|
193 |
+
End date of recurrence
|
194 |
+
:param by_second:
|
195 |
+
Second or list of seconds within a minute. Valid values are 0 to 60
|
196 |
+
:param by_minute:
|
197 |
+
Minute or list of minutes within a hour. Valid values are 0 to 59
|
198 |
+
:param by_hour:
|
199 |
+
Hour or list of hours of the day. Valid values are 0 to 23
|
200 |
+
:param by_week_day:
|
201 |
+
Day or list of days of the week.
|
202 |
+
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
203 |
+
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
204 |
+
:param by_month_day:
|
205 |
+
Day or list of days of the month. Valid values are 1 to 31 or -31 to -1.
|
206 |
+
For example, -10 represents the tenth to the last day of the month.
|
207 |
+
:param by_year_day:
|
208 |
+
Day or list of days of the year. Valid values are 1 to 366 or -366 to -1.
|
209 |
+
For example, -1 represents the last day of the year.
|
210 |
+
:param by_week:
|
211 |
+
Ordinal or list of ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to -1.
|
212 |
+
:param by_month:
|
213 |
+
Month or list of months of the year. Valid values are 1 to 12.
|
214 |
+
:param by_set_pos:
|
215 |
+
Value or list of values which corresponds to the nth occurrence within the set of events
|
216 |
+
specified by the rule. Valid values are 1 to 366 or -366 to -1.
|
217 |
+
It can only be used in conjunction with another by_xxx parameter.
|
218 |
+
:param week_start:
|
219 |
+
The day on which the workweek starts.
|
220 |
+
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
221 |
+
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
222 |
+
|
223 |
+
:return:
|
224 |
+
String representing specified recurrence rule in `RRULE format`_.
|
225 |
+
|
226 |
+
.. note:: If none of the by_day, by_month_day, or by_year_day are specified, the day is gotten from start date.
|
227 |
+
|
228 |
+
|
229 |
+
.. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
|
230 |
+
"""
|
231 |
+
return 'EXRULE:' + Recurrence._rule(freq, interval, count, until, by_second, by_minute, by_hour, by_week_day,
|
232 |
+
by_month_day, by_year_day, by_week, by_month, by_set_pos, week_start)
|
233 |
+
|
234 |
+
@staticmethod
|
235 |
+
def dates(ds):
|
236 |
+
"""Converts date(s) set to RDATE format.
|
237 |
+
|
238 |
+
:param ds:
|
239 |
+
date/datetime object or list of date/datetime objects
|
240 |
+
|
241 |
+
:return:
|
242 |
+
RDATE string of dates.
|
243 |
+
"""
|
244 |
+
return 'RDATE;' + Recurrence._dates(ds)
|
245 |
+
|
246 |
+
@staticmethod
|
247 |
+
def times(dts, timezone=get_localzone_name()):
|
248 |
+
"""Converts datetime(s) set to RDATE format.
|
249 |
+
|
250 |
+
:param dts:
|
251 |
+
datetime object or list of datetime objects
|
252 |
+
:param timezone:
|
253 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
254 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
255 |
+
|
256 |
+
:return:
|
257 |
+
RDATE string of datetimes with specified timezone.
|
258 |
+
"""
|
259 |
+
return 'RDATE;' + Recurrence._times(dts, timezone)
|
260 |
+
|
261 |
+
@staticmethod
|
262 |
+
def periods(ps, timezone=get_localzone_name()):
|
263 |
+
"""Converts date period(s) to RDATE format.
|
264 |
+
|
265 |
+
Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
|
266 |
+
(date/datetime, date/datetime/Duration)
|
267 |
+
|
268 |
+
:param ps:
|
269 |
+
Period or list of periods.
|
270 |
+
:param timezone:
|
271 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
272 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
273 |
+
|
274 |
+
:return:
|
275 |
+
RDATE string of periods.
|
276 |
+
"""
|
277 |
+
return 'RDATE;' + Recurrence._periods(ps, timezone)
|
278 |
+
|
279 |
+
@staticmethod
|
280 |
+
def exclude_dates(ds):
|
281 |
+
"""Converts date(s) set to EXDATE format.
|
282 |
+
|
283 |
+
:param ds:
|
284 |
+
date/datetime object or list of date/datetime objects
|
285 |
+
|
286 |
+
:return:
|
287 |
+
EXDATE string of dates.
|
288 |
+
"""
|
289 |
+
return 'EXDATE;' + Recurrence._dates(ds)
|
290 |
+
|
291 |
+
@staticmethod
|
292 |
+
def exclude_times(dts, timezone=get_localzone_name()):
|
293 |
+
"""Converts datetime(s) set to EXDATE format.
|
294 |
+
|
295 |
+
:param dts:
|
296 |
+
datetime object or list of datetime objects
|
297 |
+
:param timezone:
|
298 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
299 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
300 |
+
|
301 |
+
:return:
|
302 |
+
EXDATE string of datetimes with specified timezone.
|
303 |
+
"""
|
304 |
+
return 'EXDATE;' + Recurrence._times(dts, timezone)
|
305 |
+
|
306 |
+
@staticmethod
|
307 |
+
def exclude_periods(ps, timezone=get_localzone_name()):
|
308 |
+
"""Converts date period(s) to EXDATE format.
|
309 |
+
|
310 |
+
Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
|
311 |
+
(date/datetime, date/datetime/Duration)
|
312 |
+
|
313 |
+
:param ps:
|
314 |
+
Period or list of periods.
|
315 |
+
:param timezone:
|
316 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
317 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
318 |
+
|
319 |
+
:return:
|
320 |
+
EXDATE string of periods.
|
321 |
+
"""
|
322 |
+
return 'EXDATE;' + Recurrence._periods(ps, timezone)
|
323 |
+
|
324 |
+
@staticmethod
|
325 |
+
def _times(dts, timezone=get_localzone_name()):
|
326 |
+
"""Converts datetime(s) set to RDATE format.
|
327 |
+
|
328 |
+
:param dts:
|
329 |
+
datetime object or list of datetime objects
|
330 |
+
:param timezone:
|
331 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
332 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
333 |
+
|
334 |
+
:return:
|
335 |
+
RDATE string of datetimes with specified timezone.
|
336 |
+
"""
|
337 |
+
|
338 |
+
if not isinstance(dts, list):
|
339 |
+
dts = [dts]
|
340 |
+
|
341 |
+
localized_datetimes = []
|
342 |
+
for dt in dts:
|
343 |
+
if not isinstance(dt, (date, datetime)):
|
344 |
+
msg = 'The dts object(s) must be date or datetime, not {!r}.'.format(dt.__class__.__name__)
|
345 |
+
raise TypeError(msg)
|
346 |
+
localized_datetimes.append(ensure_localisation(dt, timezone))
|
347 |
+
|
348 |
+
return 'TZID={}:{}'.format(timezone, ','.join(d.strftime('%Y%m%dT%H%M%S') for d in localized_datetimes))
|
349 |
+
|
350 |
+
@staticmethod
|
351 |
+
def _dates(ds):
|
352 |
+
"""Converts date(s) set to RDATE format.
|
353 |
+
|
354 |
+
:param ds:
|
355 |
+
date/datetime object or list of date/datetime objects
|
356 |
+
|
357 |
+
:return:
|
358 |
+
RDATE string of dates.
|
359 |
+
"""
|
360 |
+
if not isinstance(ds, list):
|
361 |
+
ds = [ds]
|
362 |
+
|
363 |
+
for d in ds:
|
364 |
+
if not (isinstance(d, (date, datetime))):
|
365 |
+
msg = 'The dates object(s) must be date or datetime, not {!r}.'.format(d.__class__.__name__)
|
366 |
+
raise TypeError(msg)
|
367 |
+
|
368 |
+
return 'VALUE=DATE:' + ','.join(d.strftime('%Y%m%d') for d in ds)
|
369 |
+
|
370 |
+
@staticmethod
|
371 |
+
def _periods(ps, timezone=get_localzone_name()):
|
372 |
+
"""Converts date period(s) to RDATE format.
|
373 |
+
|
374 |
+
Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
|
375 |
+
(date/datetime, date/datetime/Duration)
|
376 |
+
|
377 |
+
:param ps:
|
378 |
+
Period or list of periods.
|
379 |
+
:param timezone:
|
380 |
+
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
381 |
+
the computers local timezone is used if it is configured. UTC is used otherwise.
|
382 |
+
|
383 |
+
:return:
|
384 |
+
RDATE string of periods.
|
385 |
+
"""
|
386 |
+
if not isinstance(ps, list):
|
387 |
+
ps = [ps]
|
388 |
+
|
389 |
+
period_strings = []
|
390 |
+
for start, end in ps:
|
391 |
+
if not isinstance(start, (date, datetime)):
|
392 |
+
msg = 'The start object(s) must be a date or datetime, not {!r}.'.format(end.__class__.__name__)
|
393 |
+
raise TypeError(msg)
|
394 |
+
|
395 |
+
start = ensure_localisation(start, timezone)
|
396 |
+
if isinstance(end, (date, datetime)):
|
397 |
+
end = ensure_localisation(end, timezone)
|
398 |
+
pstr = '{}/{}'.format(start.strftime('%Y%m%dT%H%M%SZ'), end.strftime('%Y%m%dT%H%M%SZ'))
|
399 |
+
elif isinstance(end, Duration):
|
400 |
+
pstr = '{}/{}'.format(start.strftime('%Y%m%dT%H%M%SZ'), end)
|
401 |
+
else:
|
402 |
+
msg = 'The end object(s) must be a date, datetime or Duration, not {!r}.'.format(end.__class__.__name__)
|
403 |
+
raise TypeError(msg)
|
404 |
+
period_strings.append(pstr)
|
405 |
+
|
406 |
+
return 'VALUE=PERIOD:' + ','.join(period_strings)
|
407 |
+
|
408 |
+
@staticmethod
|
409 |
+
def _rule(
|
410 |
+
freq=DAILY,
|
411 |
+
interval=None,
|
412 |
+
count=None,
|
413 |
+
until=None,
|
414 |
+
by_second=None, # BYSECOND
|
415 |
+
by_minute=None, # BYMINUTE
|
416 |
+
by_hour=None, # BYHOUR
|
417 |
+
by_week_day=None, # BYDAY
|
418 |
+
by_month_day=None, # BYMONTHDAY
|
419 |
+
by_year_day=None, # BYYEARDAY
|
420 |
+
by_week=None, # BYWEEKNO
|
421 |
+
by_month=None, # BYMONTH
|
422 |
+
by_set_pos=None, # BYSETPOS
|
423 |
+
week_start=DEFAULT_WEEK_START # WKST
|
424 |
+
):
|
425 |
+
"""This property defines a rule or repeating pattern for recurring events.
|
426 |
+
|
427 |
+
:param freq:
|
428 |
+
Identifies the type of recurrence rule. Possible values are SECONDLY, HOURLY,
|
429 |
+
MINUTELY, DAILY, WEEKLY, MONTHLY, YEARLY. Default: DAILY
|
430 |
+
:param interval:
|
431 |
+
Positive integer representing how often the recurrence rule repeats
|
432 |
+
:param count:
|
433 |
+
Number of occurrences at which to range-bound the recurrence
|
434 |
+
:param until:
|
435 |
+
End date of recurrence
|
436 |
+
:param by_second:
|
437 |
+
Second or list of seconds within a minute. Valid values are 0 to 60
|
438 |
+
:param by_minute:
|
439 |
+
Minute or list of minutes within a hour. Valid values are 0 to 59
|
440 |
+
:param by_hour:
|
441 |
+
Hour or list of hours of the day. Valid values are 0 to 23
|
442 |
+
:param by_week_day:
|
443 |
+
Day or list of days of the week.
|
444 |
+
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
445 |
+
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
446 |
+
:param by_month_day:
|
447 |
+
Day or list of days of the month. Valid values are 1 to 31 or -31 to -1.
|
448 |
+
For example, -10 represents the tenth to the last day of the month.
|
449 |
+
:param by_year_day:
|
450 |
+
Day or list of days of the year. Valid values are 1 to 366 or -366 to -1.
|
451 |
+
For example, -1 represents the last day of the year.
|
452 |
+
:param by_week:
|
453 |
+
Ordinal or list of ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to -1.
|
454 |
+
:param by_month:
|
455 |
+
Month or list of months of the year. Valid values are 1 to 12.
|
456 |
+
:param by_set_pos:
|
457 |
+
Value or list of values which corresponds to the nth occurrence within the set of events
|
458 |
+
specified by the rule. Valid values are 1 to 366 or -366 to -1.
|
459 |
+
It can only be used in conjunction with another by_xxx parameter.
|
460 |
+
:param week_start:
|
461 |
+
The day on which the workweek starts.
|
462 |
+
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
463 |
+
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
464 |
+
|
465 |
+
:return:
|
466 |
+
String representing specified recurrence rule in `RRULE format`_.
|
467 |
+
|
468 |
+
.. note:: If none of the by_day, by_month_day, or by_year_day are specified, the day is gotten from start date.
|
469 |
+
|
470 |
+
|
471 |
+
.. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
|
472 |
+
"""
|
473 |
+
|
474 |
+
def ensure_iterable(it):
|
475 |
+
return it if isinstance(it, (list, tuple, set)) else [it] if it is not None else []
|
476 |
+
|
477 |
+
def check_all_type(it, type_, name):
|
478 |
+
if any(not isinstance(o, type_) for o in it):
|
479 |
+
raise TypeError('"{}" parameter must be a {} or list of {}s.'
|
480 |
+
.format(name, type_.__name__, type_.__name__))
|
481 |
+
|
482 |
+
def check_all_type_and_range(it, type_, range_, name, nonzero=False):
|
483 |
+
check_all_type(it, type_, name)
|
484 |
+
low, high = range_
|
485 |
+
if any(not (low <= o <= high) for o in it):
|
486 |
+
raise ValueError('"{}" parameter must be in range {}-{}.'
|
487 |
+
.format(name, low, high))
|
488 |
+
if nonzero and any(o == 0 for o in it):
|
489 |
+
raise ValueError('"{}" parameter must be in range {}-{} and nonzero.'
|
490 |
+
.format(name, low, high))
|
491 |
+
|
492 |
+
def to_string(values):
|
493 |
+
return ','.join(map(str, values)) if values else None
|
494 |
+
|
495 |
+
if freq not in (SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY):
|
496 |
+
raise ValueError('"freq" parameter must be one of SECONDLY, HOURLY, MINUTELY, DAILY, '
|
497 |
+
'WEEKLY, MONTHLY or YEARLY. {} was provided'.format(freq))
|
498 |
+
if interval is not None and (not isinstance(interval, int) or interval < 1):
|
499 |
+
raise ValueError('"interval" parameter must be a positive int. '
|
500 |
+
'{} was provided'.format(interval))
|
501 |
+
if count is not None and (not isinstance(count, int) or count < 1):
|
502 |
+
raise ValueError('"count" parameter must be a positive int. '
|
503 |
+
'{} was provided'.format(count))
|
504 |
+
if until is not None:
|
505 |
+
if not isinstance(until, (date, datetime)):
|
506 |
+
raise TypeError('The until object must be a date or datetime, '
|
507 |
+
'not {!r}.'.format(until.__class__.__name__))
|
508 |
+
else:
|
509 |
+
until = until.strftime("%Y%m%dT%H%M%SZ")
|
510 |
+
if count is not None and until is not None:
|
511 |
+
raise ValueError('"count" and "until" may not appear in one recurrence rule.')
|
512 |
+
|
513 |
+
by_second = ensure_iterable(by_second)
|
514 |
+
check_all_type_and_range(by_second, int, (0, 60), "by_second")
|
515 |
+
|
516 |
+
by_minute = ensure_iterable(by_minute)
|
517 |
+
check_all_type_and_range(by_minute, int, (0, 59), "by_minute")
|
518 |
+
|
519 |
+
by_hour = ensure_iterable(by_hour)
|
520 |
+
check_all_type_and_range(by_hour, int, (0, 23), "by_hour")
|
521 |
+
|
522 |
+
by_week_day = ensure_iterable(by_week_day)
|
523 |
+
check_all_type(by_week_day, _DayOfTheWeek, "by_week_day")
|
524 |
+
|
525 |
+
by_month_day = ensure_iterable(by_month_day)
|
526 |
+
check_all_type_and_range(by_month_day, int, (-31, 31), "by_month_day", nonzero=True)
|
527 |
+
|
528 |
+
by_year_day = ensure_iterable(by_year_day)
|
529 |
+
check_all_type_and_range(by_year_day, int, (-366, 366), "by_year_day", nonzero=True)
|
530 |
+
|
531 |
+
by_week = ensure_iterable(by_week)
|
532 |
+
check_all_type_and_range(by_week, int, (-53, 53), "by_week", nonzero=True)
|
533 |
+
|
534 |
+
by_month = ensure_iterable(by_month)
|
535 |
+
check_all_type_and_range(by_month, int, (1, 12), "by_month")
|
536 |
+
|
537 |
+
by_set_pos = ensure_iterable(by_set_pos)
|
538 |
+
check_all_type_and_range(by_set_pos, int, (-366, 366), "by_set_pos", nonzero=True)
|
539 |
+
if by_set_pos and all(not v for v in (by_second, by_minute, by_hour,
|
540 |
+
by_week_day, by_month_day, by_year_day,
|
541 |
+
by_week, by_month)):
|
542 |
+
raise ValueError('"by_set_pos" parameter can only be used in conjunction with another by_xxx parameter.')
|
543 |
+
|
544 |
+
if not isinstance(week_start, _DayOfTheWeek):
|
545 |
+
raise ValueError('"week_start" parameter must be one of SUNDAY, MONDAY, etc. '
|
546 |
+
'{} was provided'.format(week_start))
|
547 |
+
|
548 |
+
rrule = 'FREQ={}'.format(freq)
|
549 |
+
|
550 |
+
rule_properties = (
|
551 |
+
('INTERVAL', interval),
|
552 |
+
('COUNT', count),
|
553 |
+
('UNTIL', until),
|
554 |
+
('BYSECOND', to_string(by_second)),
|
555 |
+
('BYMINUTE', to_string(by_minute)),
|
556 |
+
('BYHOUR', to_string(by_hour)),
|
557 |
+
('BYDAY', to_string(by_week_day)),
|
558 |
+
('BYMONTHDAY', to_string(by_month_day)),
|
559 |
+
('BYYEARDAY', to_string(by_year_day)),
|
560 |
+
('BYWEEKNO', to_string(by_week)),
|
561 |
+
('BYMONTH', to_string(by_month)),
|
562 |
+
('BYSETPOS', to_string(by_set_pos)),
|
563 |
+
('WKST', week_start)
|
564 |
+
)
|
565 |
+
|
566 |
+
for key, value in rule_properties:
|
567 |
+
if value:
|
568 |
+
rrule += ';{}={}'.format(key, value)
|
569 |
+
|
570 |
+
return rrule
|
google-calendar-simple-api/build/lib/gcsa/reminders.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import time, date, datetime
|
2 |
+
from typing import Union
|
3 |
+
|
4 |
+
from beautiful_date import BeautifulDate, days
|
5 |
+
|
6 |
+
|
7 |
+
class Reminder:
|
8 |
+
def __init__(
|
9 |
+
self,
|
10 |
+
method: str,
|
11 |
+
minutes_before_start: int = None,
|
12 |
+
days_before: int = None,
|
13 |
+
at: time = None
|
14 |
+
):
|
15 |
+
"""Represents base reminder object
|
16 |
+
|
17 |
+
Provide `minutes_before_start` to create "relative" reminder.
|
18 |
+
Provide `days_before` and `at` to create "absolute" reminder.
|
19 |
+
|
20 |
+
:param method:
|
21 |
+
Method of the reminder. Possible values: email or popup
|
22 |
+
:param minutes_before_start:
|
23 |
+
Minutes before reminder
|
24 |
+
:param days_before:
|
25 |
+
Days before reminder
|
26 |
+
:param at:
|
27 |
+
Specific time for a reminder
|
28 |
+
"""
|
29 |
+
# Nothing was provided
|
30 |
+
if minutes_before_start is None and days_before is None and at is None:
|
31 |
+
raise ValueError("Relative reminder needs 'minutes_before_start'. "
|
32 |
+
"Absolute reminder 'days_before' and 'at' set. "
|
33 |
+
"None of them were provided.")
|
34 |
+
|
35 |
+
# Both minutes_before_start and days_before/at were provided
|
36 |
+
if minutes_before_start is not None and (days_before is not None or at is not None):
|
37 |
+
raise ValueError("Only minutes_before_start or days_before/at can be specified.")
|
38 |
+
|
39 |
+
# Only one of days_before and at was provided
|
40 |
+
if (days_before is None) != (at is None):
|
41 |
+
raise ValueError(f'Both "days_before" and "at" values need to be set '
|
42 |
+
f'when using absolute time for a reminder. '
|
43 |
+
f'Provided days_before={days_before} and at={at}.')
|
44 |
+
|
45 |
+
self.method = method
|
46 |
+
self.minutes_before_start = minutes_before_start
|
47 |
+
self.days_before = days_before
|
48 |
+
self.at = at
|
49 |
+
|
50 |
+
def __eq__(self, other):
|
51 |
+
return (
|
52 |
+
isinstance(other, Reminder)
|
53 |
+
and self.method == other.method
|
54 |
+
and self.minutes_before_start == other.minutes_before_start
|
55 |
+
and self.days_before == other.days_before
|
56 |
+
and self.at == other.at
|
57 |
+
)
|
58 |
+
|
59 |
+
def __str__(self):
|
60 |
+
if self.minutes_before_start is not None:
|
61 |
+
return '{} - minutes_before_start:{}'.format(self.__class__.__name__, self.minutes_before_start)
|
62 |
+
else:
|
63 |
+
return '{} - {} days before at {}'.format(self.__class__.__name__, self.days_before, self.at)
|
64 |
+
|
65 |
+
def __repr__(self):
|
66 |
+
return '<{}>'.format(self.__str__())
|
67 |
+
|
68 |
+
def convert_to_relative(self, start: Union[date, datetime, BeautifulDate]) -> 'Reminder':
|
69 |
+
"""Converts absolute reminder (with set `days_before` and `at`) to relative (with set `minutes_before_start`)
|
70 |
+
relative to `start` date/datetime. Returns self if `minutes_before_start` already set.
|
71 |
+
"""
|
72 |
+
if self.minutes_before_start is not None:
|
73 |
+
return self
|
74 |
+
|
75 |
+
tzinfo = start.tzinfo if isinstance(start, datetime) else None
|
76 |
+
start_of_the_day = datetime.combine(start, datetime.min.time(), tzinfo=tzinfo)
|
77 |
+
|
78 |
+
reminder_tzinfo = self.at.tzinfo or tzinfo
|
79 |
+
reminder_time = datetime.combine(start_of_the_day - self.days_before * days, self.at, tzinfo=reminder_tzinfo)
|
80 |
+
|
81 |
+
if isinstance(start, datetime):
|
82 |
+
minutes_before_start = int((start - reminder_time).total_seconds() / 60)
|
83 |
+
else:
|
84 |
+
minutes_before_start = int((start_of_the_day - reminder_time).total_seconds() / 60)
|
85 |
+
|
86 |
+
return Reminder(
|
87 |
+
method=self.method,
|
88 |
+
minutes_before_start=minutes_before_start
|
89 |
+
)
|
90 |
+
|
91 |
+
|
92 |
+
class EmailReminder(Reminder):
|
93 |
+
def __init__(
|
94 |
+
self,
|
95 |
+
minutes_before_start: int = None,
|
96 |
+
days_before: int = None,
|
97 |
+
at: time = None
|
98 |
+
):
|
99 |
+
"""Represents email reminder object
|
100 |
+
|
101 |
+
Provide `minutes_before_start` to create "relative" reminder.
|
102 |
+
Provide `days_before` and `at` to create "absolute" reminder.
|
103 |
+
|
104 |
+
:param minutes_before_start:
|
105 |
+
Minutes before reminder
|
106 |
+
:param days_before:
|
107 |
+
Days before reminder
|
108 |
+
:param at:
|
109 |
+
Specific time for a reminder
|
110 |
+
"""
|
111 |
+
if not days_before and not at and not minutes_before_start:
|
112 |
+
minutes_before_start = 60
|
113 |
+
super().__init__('email', minutes_before_start, days_before, at)
|
114 |
+
|
115 |
+
|
116 |
+
class PopupReminder(Reminder):
|
117 |
+
def __init__(
|
118 |
+
self,
|
119 |
+
minutes_before_start: int = None,
|
120 |
+
days_before: int = None,
|
121 |
+
at: time = None
|
122 |
+
):
|
123 |
+
"""Represents popup reminder object
|
124 |
+
|
125 |
+
Provide `minutes_before_start` to create "relative" reminder.
|
126 |
+
Provide `days_before` and `at` to create "absolute" reminder.
|
127 |
+
|
128 |
+
:param minutes_before_start:
|
129 |
+
Minutes before reminder
|
130 |
+
:param days_before:
|
131 |
+
Days before reminder
|
132 |
+
:param at:
|
133 |
+
Specific time for a reminder
|
134 |
+
"""
|
135 |
+
if not days_before and not at and not minutes_before_start:
|
136 |
+
minutes_before_start = 30
|
137 |
+
super().__init__('popup', minutes_before_start, days_before, at)
|