reservations: Add task to sync with Google Calendar
This commit is contained in:
parent
075812face
commit
35c063c44e
@ -18,6 +18,10 @@ class Base(Configuration):
|
|||||||
credentials_directory = os.getenv("CREDENTIALS_DIRECTORY")
|
credentials_directory = os.getenv("CREDENTIALS_DIRECTORY")
|
||||||
if credentials_directory is not None:
|
if credentials_directory is not None:
|
||||||
for credential in Path(credentials_directory).iterdir():
|
for credential in Path(credentials_directory).iterdir():
|
||||||
|
if credential.name.endswith("_path"):
|
||||||
|
os.environ.setdefault(
|
||||||
|
credential.name.removesuffix("_path"), str(credential.resolve())
|
||||||
|
)
|
||||||
if credential.name.isupper():
|
if credential.name.isupper():
|
||||||
os.environ.setdefault(credential.name, credential.read_text())
|
os.environ.setdefault(credential.name, credential.read_text())
|
||||||
|
|
||||||
@ -226,6 +230,8 @@ class NonCIBase(Base):
|
|||||||
environ_required=True, environ_prefix=None
|
environ_required=True, environ_prefix=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_FILE = values.PathValue(environ_prefix=None)
|
||||||
|
|
||||||
HID_DOOR_USERNAME = values.Value(environ_required=True, environ_prefix=None)
|
HID_DOOR_USERNAME = values.Value(environ_required=True, environ_prefix=None)
|
||||||
HID_DOOR_PASSWORD = values.SecretValue(environ_prefix=None)
|
HID_DOOR_PASSWORD = values.SecretValue(environ_prefix=None)
|
||||||
|
|
||||||
|
255
pdm.lock
255
pdm.lock
@ -5,7 +5,7 @@
|
|||||||
groups = ["default", "debug", "dev", "lint", "server", "typing"]
|
groups = ["default", "debug", "dev", "lint", "server", "typing"]
|
||||||
strategy = ["inherit_metadata"]
|
strategy = ["inherit_metadata"]
|
||||||
lock_version = "4.5.0"
|
lock_version = "4.5.0"
|
||||||
content_hash = "sha256:cdef77160cecd840eeaef2f45c9d4afc0701146cf0b78e8b5cb782bc57144ad7"
|
content_hash = "sha256:b76b2cc9bd24beecef27ef102062710607993f9b4c318e7bda89b2217efccd29"
|
||||||
|
|
||||||
[[metadata.targets]]
|
[[metadata.targets]]
|
||||||
requires_python = "==3.11.*"
|
requires_python = "==3.11.*"
|
||||||
@ -187,6 +187,18 @@ files = [
|
|||||||
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
|
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "5.4.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Extensible memoizing collections and decorators"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
|
||||||
|
{file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2024.7.4"
|
version = "2024.7.4"
|
||||||
@ -534,6 +546,21 @@ files = [
|
|||||||
{file = "django_markdownx-4.0.7-py2.py3-none-any.whl", hash = "sha256:c1975ae3053481d4c111abd38997a5b5bb89235a1e3215f995d835942925fe7b"},
|
{file = "django_markdownx-4.0.7-py2.py3-none-any.whl", hash = "sha256:c1975ae3053481d4c111abd38997a5b5bb89235a1e3215f995d835942925fe7b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-model-utils"
|
||||||
|
version = "4.5.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Django model mixins and utilities"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"Django>=3.2",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "django_model_utils-4.5.1-py3-none-any.whl", hash = "sha256:f1141fc71796242edeffed5ad53a8cc57f00d345eb5a3a63e3f69401cd562ee2"},
|
||||||
|
{file = "django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-mysql"
|
name = "django-mysql"
|
||||||
version = "4.14.0"
|
version = "4.14.0"
|
||||||
@ -705,6 +732,20 @@ files = [
|
|||||||
{file = "django_tables2-2.7.0-py2.py3-none-any.whl", hash = "sha256:99e06d966ca8ac69fd74092eb45c79a280dd5ca0ccb81395d96261f62128e1af"},
|
{file = "django_tables2-2.7.0-py2.py3-none-any.whl", hash = "sha256:99e06d966ca8ac69fd74092eb45c79a280dd5ca0ccb81395d96261f62128e1af"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-template-partials"
|
||||||
|
version = "24.2"
|
||||||
|
summary = "django-template-partials"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"Django",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "django-template-partials-24.2.tar.gz", hash = "sha256:e594063faec6d012df3a4170edf3c5dcc07c82b1c311aa42a1a5838493fd3a72"},
|
||||||
|
{file = "django_template_partials-24.2-py2.py3-none-any.whl", hash = "sha256:b859072e6b3cd780743399bf5e9cee8be1c56c88844425a4e669a65c136205b2"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-vite"
|
name = "django-vite"
|
||||||
version = "3.0.4"
|
version = "3.0.4"
|
||||||
@ -925,6 +966,107 @@ files = [
|
|||||||
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
|
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-api-core"
|
||||||
|
version = "2.19.1"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Google API client core library"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"google-auth<3.0.dev0,>=2.14.1",
|
||||||
|
"googleapis-common-protos<2.0.dev0,>=1.56.2",
|
||||||
|
"proto-plus<2.0.0dev,>=1.22.3",
|
||||||
|
"protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.19.5",
|
||||||
|
"requests<3.0.0.dev0,>=2.18.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd"},
|
||||||
|
{file = "google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-api-python-client"
|
||||||
|
version = "2.139.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Google API Client Library for Python"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0,>=1.31.5",
|
||||||
|
"google-auth!=2.24.0,!=2.25.0,<3.0.0.dev0,>=1.32.0",
|
||||||
|
"google-auth-httplib2<1.0.0,>=0.2.0",
|
||||||
|
"httplib2<1.dev0,>=0.19.0",
|
||||||
|
"uritemplate<5,>=3.0.1",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "google_api_python_client-2.139.0-py2.py3-none-any.whl", hash = "sha256:1850a92505d91a82e2ca1635ab2b8dff179f4b67082c2651e1db332e8039840c"},
|
||||||
|
{file = "google_api_python_client-2.139.0.tar.gz", hash = "sha256:ed4bc3abe2c060a87412465b4e8254620bbbc548eefc5388e2c5ff912d36a68b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth"
|
||||||
|
version = "2.32.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Google Authentication Library"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"cachetools<6.0,>=2.0.0",
|
||||||
|
"pyasn1-modules>=0.2.1",
|
||||||
|
"rsa<5,>=3.1.4",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "google_auth-2.32.0-py2.py3-none-any.whl", hash = "sha256:53326ea2ebec768070a94bee4e1b9194c9646ea0c2bd72422785bd0f9abfad7b"},
|
||||||
|
{file = "google_auth-2.32.0.tar.gz", hash = "sha256:49315be72c55a6a37d62819e3573f6b416aca00721f7e3e31a008d928bf64022"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth-httplib2"
|
||||||
|
version = "0.2.0"
|
||||||
|
summary = "Google Authentication Library: httplib2 transport"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"google-auth",
|
||||||
|
"httplib2>=0.19.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"},
|
||||||
|
{file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth-oauthlib"
|
||||||
|
version = "1.2.1"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "Google Authentication Library"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"google-auth>=2.15.0",
|
||||||
|
"requests-oauthlib>=0.7.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"},
|
||||||
|
{file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "googleapis-common-protos"
|
||||||
|
version = "1.63.2"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Common protobufs used in Google APIs"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"},
|
||||||
|
{file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@ -980,6 +1122,22 @@ files = [
|
|||||||
{file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
|
{file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httplib2"
|
||||||
|
version = "0.22.0"
|
||||||
|
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
summary = "A comprehensive HTTP client library."
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2; python_version > \"3.0\"",
|
||||||
|
"pyparsing<3,>=2.4.2; python_version < \"3.0\"",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"},
|
||||||
|
{file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httptools"
|
name = "httptools"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@ -1286,6 +1444,18 @@ files = [
|
|||||||
{file = "nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4"},
|
{file = "nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oauthlib"
|
||||||
|
version = "3.2.2"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
|
||||||
|
{file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "odfpy"
|
name = "odfpy"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@ -1409,6 +1579,34 @@ files = [
|
|||||||
{file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"},
|
{file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proto-plus"
|
||||||
|
version = "1.24.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Beautiful, Pythonic protocol buffers."
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"protobuf<6.0.0dev,>=3.19.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"},
|
||||||
|
{file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "5.27.3"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = ""
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"},
|
||||||
|
{file = "protobuf-5.27.3-py3-none-any.whl", hash = "sha256:8572c6533e544ebf6899c360e91d6bcbbee2549251643d32c52cf8a5de295ba5"},
|
||||||
|
{file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ptyprocess"
|
name = "ptyprocess"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -1494,6 +1692,18 @@ files = [
|
|||||||
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyparsing"
|
||||||
|
version = "3.1.2"
|
||||||
|
requires_python = ">=3.6.8"
|
||||||
|
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
|
||||||
|
{file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyphen"
|
name = "pyphen"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@ -1590,6 +1800,37 @@ files = [
|
|||||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-oauthlib"
|
||||||
|
version = "2.0.0"
|
||||||
|
requires_python = ">=3.4"
|
||||||
|
summary = "OAuthlib authentication support for Requests."
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"oauthlib>=3.0.0",
|
||||||
|
"requests>=2.0.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"},
|
||||||
|
{file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "4.9"
|
||||||
|
requires_python = ">=3.6,<4"
|
||||||
|
summary = "Pure-Python RSA implementation"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"pyasn1>=0.1.3",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
|
||||||
|
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@ -1977,6 +2218,18 @@ files = [
|
|||||||
{file = "udm_rest_client-1.2.3-py2.py3-none-any.whl", hash = "sha256:ee29e94e3ba5fba63a694e33d119b1af7450afcdce3a44301d4cd5ddfa1f980b"},
|
{file = "udm_rest_client-1.2.3-py2.py3-none-any.whl", hash = "sha256:ee29e94e3ba5fba63a694e33d119b1af7450afcdce3a44301d4cd5ddfa1f980b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uritemplate"
|
||||||
|
version = "4.1.1"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "Implementation of RFC 6570 URI Templates"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
|
||||||
|
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.2.2"
|
version = "2.2.2"
|
||||||
|
@ -40,6 +40,10 @@ dependencies = [
|
|||||||
"django-bootstrap5~=24.2",
|
"django-bootstrap5~=24.2",
|
||||||
"django-configurations[database,email]~=2.5",
|
"django-configurations[database,email]~=2.5",
|
||||||
"django-vite~=3.0",
|
"django-vite~=3.0",
|
||||||
|
"django-template-partials~=24.2",
|
||||||
|
"google-api-python-client~=2.139",
|
||||||
|
"google-auth-oauthlib~=1.2",
|
||||||
|
"django-model-utils~=4.5",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
@ -80,6 +84,7 @@ indent = 2
|
|||||||
blank_line_after_tag = "load,extends"
|
blank_line_after_tag = "load,extends"
|
||||||
max_blank_lines = 1
|
max_blank_lines = 1
|
||||||
ignore = "T003,H017,H021,H030,H031"
|
ignore = "T003,H017,H021,H030,H031"
|
||||||
|
custom_blocks = "partialdef"
|
||||||
format_css = true
|
format_css = true
|
||||||
format_js = true
|
format_js = true
|
||||||
|
|
||||||
|
@ -1,6 +1,24 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.db.models.signals import post_migrate
|
||||||
|
|
||||||
|
|
||||||
|
def post_migrate_callback(sender, **kwargs):
|
||||||
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
from cmsmanage.django_q2_helper import ensure_scheduled
|
||||||
|
|
||||||
|
from .tasks.sync_google_calendar import sync_reservations_with_google_calendar
|
||||||
|
|
||||||
|
ensure_scheduled(
|
||||||
|
sync_reservations_with_google_calendar,
|
||||||
|
schedule_type=Schedule.MINUTES,
|
||||||
|
minutes=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReservationsConfig(AppConfig):
|
class ReservationsConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "reservations"
|
name = "reservations"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
post_migrate.connect(post_migrate_callback, sender=self)
|
||||||
|
0
reservations/management/__init__.py
Normal file
0
reservations/management/__init__.py
Normal file
0
reservations/management/commands/__init__.py
Normal file
0
reservations/management/commands/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from reservations.tasks.sync_google_calendar import (
|
||||||
|
logger,
|
||||||
|
sync_reservations_with_google_calendar,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, verbosity: int, **options):
|
||||||
|
verbosity_levels = {
|
||||||
|
0: logging.ERROR,
|
||||||
|
1: logging.WARNING,
|
||||||
|
2: logging.INFO,
|
||||||
|
3: logging.DEBUG,
|
||||||
|
}
|
||||||
|
logger.setLevel(verbosity_levels.get(verbosity, logging.WARNING))
|
||||||
|
|
||||||
|
sync_reservations_with_google_calendar()
|
0
reservations/tasks/__init__.py
Normal file
0
reservations/tasks/__init__.py
Normal file
189
reservations/tasks/sync_google_calendar.py
Normal file
189
reservations/tasks/sync_google_calendar.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import date, datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from google.oauth2 import service_account
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
|
||||||
|
from cmsmanage.django_q2_helper import q_task_group
|
||||||
|
from reservations.models import Reservation, Resource
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCOPES = ["https://www.googleapis.com/auth/calendar"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_google_calendar_datetime(dt) -> date | datetime:
|
||||||
|
if "date" in dt:
|
||||||
|
return date.fromisoformat(dt["date"])
|
||||||
|
elif "dateTime" in dt:
|
||||||
|
return datetime.fromisoformat(dt["dateTime"])
|
||||||
|
else:
|
||||||
|
raise Exception("Google Calendar event with out a start/end date/dateTime")
|
||||||
|
|
||||||
|
|
||||||
|
def update_calendar_event(
|
||||||
|
service, resource: Resource, existing_event, reservation: Reservation
|
||||||
|
):
|
||||||
|
changes = reservation.make_google_calendar_event()
|
||||||
|
# skip update if no changes are needed
|
||||||
|
if (
|
||||||
|
parse_google_calendar_datetime(existing_event["start"]) != reservation.start
|
||||||
|
or parse_google_calendar_datetime(existing_event["end"]) != reservation.end
|
||||||
|
or any(
|
||||||
|
existing_event[k] != v
|
||||||
|
for k, v in changes.items()
|
||||||
|
if k not in ("start", "end")
|
||||||
|
)
|
||||||
|
):
|
||||||
|
logger.debug("Updating event")
|
||||||
|
new_event = existing_event | changes
|
||||||
|
service.events().update(
|
||||||
|
calendarId=resource.google_calendar,
|
||||||
|
eventId=reservation.google_calendar_event_id,
|
||||||
|
body=new_event,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
|
||||||
|
def insert_calendar_event(service, resource: Resource, reservation: Reservation):
|
||||||
|
new_gcal_event = reservation.make_google_calendar_event()
|
||||||
|
created_event = (
|
||||||
|
service.events()
|
||||||
|
.insert(
|
||||||
|
calendarId=resource.google_calendar,
|
||||||
|
body=new_gcal_event,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
reservation.google_calendar_event_id = created_event["id"]
|
||||||
|
reservation.save()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_resource_from_google_calendar(
|
||||||
|
service, resource: Resource, now: datetime
|
||||||
|
) -> set[str]:
|
||||||
|
request = (
|
||||||
|
service.events()
|
||||||
|
.list(
|
||||||
|
calendarId=resource.google_calendar,
|
||||||
|
timeMin=now.isoformat(timespec="seconds"),
|
||||||
|
maxResults=2500,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
if "nextPageToken" in request:
|
||||||
|
# TODO: implement pagination
|
||||||
|
raise Exception(
|
||||||
|
"More events than fit on a page, and pagination not implemented"
|
||||||
|
)
|
||||||
|
events = request["items"]
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
if (
|
||||||
|
"extendedProperties" in event
|
||||||
|
and "private" in event["extendedProperties"]
|
||||||
|
and event["extendedProperties"]["private"].get("cmsmanage") == "1"
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
reservation = resource.reservation_set.get_subclass(
|
||||||
|
google_calendar_event_id=event["id"]
|
||||||
|
)
|
||||||
|
# event exists in both Google Calendar and database, check for update
|
||||||
|
logger.debug(
|
||||||
|
"Event in Google Calendar found in database: checking for update | %s",
|
||||||
|
event["id"],
|
||||||
|
)
|
||||||
|
update_calendar_event(service, resource, event, reservation)
|
||||||
|
except Reservation.DoesNotExist:
|
||||||
|
# reservation deleted in database, so remove from Google Calendar
|
||||||
|
logger.info(
|
||||||
|
"Event in Google Calendar not found in database: deleting | %s",
|
||||||
|
event["id"],
|
||||||
|
)
|
||||||
|
service.events().delete(
|
||||||
|
calendarId=resource.google_calendar,
|
||||||
|
eventId=event["id"],
|
||||||
|
sendUpdates="none",
|
||||||
|
).execute()
|
||||||
|
else:
|
||||||
|
# TODO: handle external events (either Bookly or manually created)
|
||||||
|
# NOTE: this will also need to check for deleted events
|
||||||
|
logger.debug(
|
||||||
|
"Event in Google Calendar not originated by CMSManage: skipping for now | %s",
|
||||||
|
event["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {event["id"] for event in events}
|
||||||
|
|
||||||
|
|
||||||
|
def sync_resource_from_database(
|
||||||
|
service, resource: Resource, now: datetime, existing_event_ids: set[str]
|
||||||
|
):
|
||||||
|
reservations = resource.reservation_set.filter(end__gt=now).select_subclasses()
|
||||||
|
# TODO: this could probably be more efficient?
|
||||||
|
for reservation in reservations:
|
||||||
|
if not reservation.google_calendar_event_id:
|
||||||
|
logger.info(
|
||||||
|
"Event in database has no Google Calendar event ID: inserting | %s",
|
||||||
|
reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
insert_calendar_event(service, resource, reservation)
|
||||||
|
|
||||||
|
# reservation has an event id, so check if we already handled it earlier
|
||||||
|
elif reservation.google_calendar_event_id not in existing_event_ids:
|
||||||
|
# this event was in Google Calendar at some point (possibly for a different
|
||||||
|
# resource/calendar), but did not appear in list(). Try to update it, then
|
||||||
|
# fall back to insert
|
||||||
|
logger.info(
|
||||||
|
"Reservation with event id not in Google Calendar: trying update | %s",
|
||||||
|
reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
event = (
|
||||||
|
service.events()
|
||||||
|
.get(
|
||||||
|
calendarId=resource.google_calendar,
|
||||||
|
eventId=reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
update_calendar_event(service, resource, event, reservation)
|
||||||
|
except HttpError as error:
|
||||||
|
if error.status_code == HTTPStatus.NOT_FOUND:
|
||||||
|
logger.info(
|
||||||
|
"Event in database not in Google Calendar: inserting | %s",
|
||||||
|
reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
insert_calendar_event(service, resource, reservation)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def sync_resource(service, resource: Resource, now: datetime):
|
||||||
|
logger.info(
|
||||||
|
"Checking calendar %s for resource %s", resource.google_calendar, resource
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_event_ids = sync_resource_from_google_calendar(service, resource, now)
|
||||||
|
sync_resource_from_database(service, resource, now, existing_event_ids)
|
||||||
|
|
||||||
|
|
||||||
|
@q_task_group("Sync Reservations with Google Calendar")
|
||||||
|
def sync_reservations_with_google_calendar():
|
||||||
|
service = build(
|
||||||
|
"calendar",
|
||||||
|
"v3",
|
||||||
|
credentials=service_account.Credentials.from_service_account_file(
|
||||||
|
settings.GOOGLE_SERVICE_ACCOUNT_FILE,
|
||||||
|
scopes=SCOPES,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
for resource in Resource.objects.all():
|
||||||
|
sync_resource(service, resource, now)
|
Loading…
Reference in New Issue
Block a user