diff --git a/docs/mailinglists.rst b/docs/mailinglists.rst index 64719a69786099b1f8c46a0c373d537e72fca2c6..2041f3abc17385d908f6e2300b4584a66e69c0d8 100644 --- a/docs/mailinglists.rst +++ b/docs/mailinglists.rst @@ -32,6 +32,14 @@ mailinglists.apps module :undoc-members: :show-inheritance: +mailinglists.gsuite module +-------------------------- + +.. automodule:: mailinglists.gsuite + :members: + :undoc-members: + :show-inheritance: + mailinglists.models module -------------------------- @@ -48,3 +56,11 @@ mailinglists.services module :undoc-members: :show-inheritance: +mailinglists.signals module +--------------------------- + +.. automodule:: mailinglists.signals + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/utils.rst b/docs/utils.rst index 20a0241c8907fd6924b7e0e10f0aa6a66277a0c2..21d642cf41da863e030f446cdf746cb44043002e 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -43,6 +43,14 @@ utils.exception\_filter module :undoc-members: :show-inheritance: +utils.google\_api module +------------------------ + +.. automodule:: utils.google_api + :members: + :undoc-members: + :show-inheritance: + utils.snippets module --------------------- diff --git a/poetry.lock b/poetry.lock index 49d876c3669952cad482d0cfd10e1b722173e22c..94eb490e699a773a47c0ff289baff56dc92c6711 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,19 +18,27 @@ version = "19.1.0" cffi = ">=1.0.0" six = "*" +[package.extras] +dev = ["coverage", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"] +docs = ["sphinx"] +tests = ["coverage", "hypothesis", "pytest"] + [[package]] category = "dev" description = "An abstract syntax tree for Python with inference support." name = "astroid" optional = false -python-versions = ">=3.4.*" -version = "2.2.5" +python-versions = ">=3.5.*" +version = "2.3.2" [package.dependencies] -lazy-object-proxy = "*" -six = "*" -typed-ast = ">=1.3.0" -wrapt = "*" +lazy-object-proxy = ">=1.4.0,<1.5.0" +six = "1.12" +wrapt = ">=1.11.0,<1.12.0" + +[package.dependencies.typed-ast] +python = "<3.8" +version = ">=1.4.0,<1.5" [[package]] category = "main" @@ -38,10 +46,10 @@ description = "Internationalization utilities" name = "babel" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.6.0" +version = "2.7.0" [package.dependencies] -pytz = ">=0a" +pytz = ">=2015.7" [[package]] category = "main" @@ -49,23 +57,30 @@ description = "Modern password hashing for your software and your servers" name = "bcrypt" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.1.6" +version = "3.1.7" [package.dependencies] cffi = ">=1.1" six = ">=1.4.1" +[package.extras] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"] + [[package]] category = "main" description = "Screen-scraping library" name = "beautifulsoup4" optional = false python-versions = "*" -version = "4.8.0" +version = "4.8.1" [package.dependencies] soupsieve = ">=1.2" +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] category = "main" description = "An easy safelist-based HTML-sanitizing tool." @@ -90,13 +105,17 @@ version = "0.12.5" msgpack = "*" requests = "*" +[package.extras] +filecache = ["lockfile (>=0.9)"] +redis = ["redis (>=2.10.5)"] + [[package]] category = "main" description = "Extensible memoizing collections and decorators" name = "cachetools" optional = false python-versions = "*" -version = "3.1.0" +version = "3.1.1" [[package]] category = "main" @@ -104,7 +123,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2019.3.9" +version = "2019.9.11" [[package]] category = "main" @@ -112,7 +131,7 @@ description = "Foreign Function Interface for Python calling C code." name = "cffi" optional = false python-versions = "*" -version = "1.12.3" +version = "1.13.0" [package.dependencies] pycparser = "*" @@ -140,10 +159,10 @@ description = "Python parser for the CommonMark Markdown spec" name = "commonmark" optional = true python-versions = "*" -version = "0.9.0" +version = "0.9.1" -[package.dependencies] -future = "*" +[package.extras] +test = ["flake8 (3.7.8)", "hypothesis (3.55.3)"] [[package]] category = "dev" @@ -151,7 +170,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" -version = "4.5.3" +version = "4.5.4" [[package]] category = "main" @@ -159,12 +178,16 @@ description = "A high-level Python Web framework that encourages rapid developme name = "django" optional = false python-versions = ">=3.5" -version = "2.2.1" +version = "2.2.6" [package.dependencies] pytz = "*" sqlparse = "*" +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + [[package]] category = "main" description = "A helper class for handling configuration defaults of packaged apps gracefully." @@ -183,7 +206,7 @@ description = "Bootstrap support for Django projects" name = "django-bootstrap4" optional = false python-versions = "*" -version = "1.0.0" +version = "1.0.1" [package.dependencies] beautifulsoup4 = "*" @@ -242,7 +265,7 @@ description = "Abstraction to offload file uploads to web-server (e.g. Apache wi name = "django-sendfile2" optional = false python-versions = "*" -version = "0.4.2" +version = "0.4.3" [package.dependencies] django = "*" @@ -277,15 +300,15 @@ description = "Web APIs for Django, made easy." name = "djangorestframework" optional = false python-versions = ">=3.5" -version = "3.10.0" +version = "3.10.3" [[package]] category = "main" description = "Docutils -- Python Documentation Utilities" name = "docutils" optional = true -python-versions = "*" -version = "0.14" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.15.2" [[package]] category = "dev" @@ -312,12 +335,12 @@ description = "Faker is a Python package that generates fake data for you." name = "faker" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.0" +version = "2.0.3" [package.dependencies] python-dateutil = ">=2.4" six = ">=1.10" -text-unidecode = "1.2" +text-unidecode = "1.3" [[package]] category = "main" @@ -329,19 +352,22 @@ version = "3.0.0" [package.dependencies] cachecontrol = ">=0.12.4" -google-api-core = ">=1.14.0,<2.0.0dev" google-api-python-client = ">=1.7.8" google-cloud-firestore = ">=1.4.0" google-cloud-storage = ">=1.18.0" six = ">=1.6.1" +[package.dependencies.google-api-core] +extras = ["grpc"] +version = ">=1.14.0,<2.0.0dev" + [[package]] category = "dev" description = "the modular source code checker: pep8, pyflakes and co" name = "flake8" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.7" +version = "3.7.8" [package.dependencies] entrypoints = ">=0.3.0,<0.4.0" @@ -355,27 +381,19 @@ description = "Let your Python tests travel through time" name = "freezegun" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.3.11" +version = "0.3.12" [package.dependencies] python-dateutil = ">=1.0,<2.0 || >2.0" six = "*" -[[package]] -category = "main" -description = "Clean single-source support for Python 3 and 2" -name = "future" -optional = true -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.17.1" - [[package]] category = "main" description = "Google API client core library" name = "google-api-core" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.14.2" +version = "1.14.3" [package.dependencies] google-auth = ">=0.4.0,<2.0dev" @@ -386,13 +404,22 @@ requests = ">=2.18.0,<3.0.0dev" setuptools = ">=34.0.0" six = ">=1.10.0" +[package.dependencies.grpcio] +optional = true +version = ">=1.8.2,<2.0dev" + +[package.extras] +grpc = ["grpcio (>=1.8.2,<2.0dev)"] +grpcgcp = ["grpcio-gcp (>=0.2.2)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] + [[package]] category = "main" description = "Google API Client Library for Python" name = "google-api-python-client" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.7.9" +version = "1.7.11" [package.dependencies] google-auth = ">=1.4.1" @@ -438,6 +465,9 @@ version = "1.0.3" [package.dependencies] google-api-core = ">=1.14.0,<2.0.0dev" +[package.extras] +grpc = ["grpcio (>=1.8.2,<2.0dev)"] + [[package]] category = "main" description = "Google Cloud Firestore API client library" @@ -445,25 +475,28 @@ marker = "platform_python_implementation != \"PyPy\"" name = "google-cloud-firestore" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.4.0" +version = "1.5.0" [package.dependencies] -google-api-core = ">=1.14.0,<2.0.0dev" -google-cloud-core = ">=1.0.0,<2.0dev" +google-cloud-core = ">=1.0.3,<2.0dev" pytz = "*" +[package.dependencies.google-api-core] +extras = ["grpc"] +version = ">=1.14.0,<2.0.0dev" + [[package]] category = "main" description = "Google Cloud Storage API client library" name = "google-cloud-storage" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.19.0" +version = "1.20.0" [package.dependencies] google-auth = ">=1.2.0" google-cloud-core = ">=1.0.3,<2.0dev" -google-resumable-media = ">=0.3.1" +google-resumable-media = ">=0.3.1,<0.4.0 || >0.4.0,<0.5dev" [[package]] category = "main" @@ -471,11 +504,14 @@ description = "Utilities for Google Media Downloads and Resumable Uploads" name = "google-resumable-media" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "0.3.2" +version = "0.4.1" [package.dependencies] six = "*" +[package.extras] +requests = ["requests (>=2.18.0,<3.0.0dev)"] + [[package]] category = "main" description = "Common protobufs used in Google APIs" @@ -487,13 +523,28 @@ version = "1.6.0" [package.dependencies] protobuf = ">=3.6.0" +[package.extras] +grpc = ["grpcio (>=1.0.0)"] + +[[package]] +category = "main" +description = "HTTP/2-based RPC framework" +marker = "platform_python_implementation != \"PyPy\" and extra == \"grpc\"" +name = "grpcio" +optional = false +python-versions = "*" +version = "1.24.1" + +[package.dependencies] +six = ">=1.5.2" + [[package]] category = "main" description = "A comprehensive HTTP client library." name = "httplib2" optional = false python-versions = "*" -version = "0.12.3" +version = "0.14.0" [[package]] category = "main" @@ -529,19 +580,28 @@ description = "A Python utility / library to sort Python imports." name = "isort" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "4.3.20" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] [[package]] category = "main" -description = "A small but fast and easy to use stand-alone template engine written in pure python." +description = "A very fast and expressive template engine." name = "jinja2" optional = true python-versions = "*" -version = "2.10.1" +version = "2.10.3" [package.dependencies] MarkupSafe = ">=0.23" +[package.extras] +i18n = ["Babel (>=0.8)"] + [[package]] category = "main" description = "JavaScript minifier." @@ -556,7 +616,7 @@ description = "A fast and thorough lazy object proxy." name = "lazy-object-proxy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.1" +version = "1.4.2" [[package]] category = "main" @@ -564,7 +624,7 @@ description = "Sass for Python: A straightforward binding of libsass for Python. name = "libsass" optional = false python-versions = "*" -version = "0.19.1" +version = "0.19.3" [package.dependencies] six = "*" @@ -591,7 +651,7 @@ description = "MessagePack (de)serializer." name = "msgpack" optional = false python-versions = "*" -version = "0.6.1" +version = "0.6.2" [[package]] category = "main" @@ -599,7 +659,7 @@ description = "Core utilities for Python packages" name = "packaging" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.0" +version = "19.2" [package.dependencies] pyparsing = ">=2.0.2" @@ -619,7 +679,7 @@ description = "Protocol Buffers" name = "protobuf" optional = false python-versions = "*" -version = "3.7.1" +version = "3.10.0" [package.dependencies] setuptools = "*" @@ -631,7 +691,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" name = "psycopg2-binary" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.8.2" +version = "2.8.3" [[package]] category = "main" @@ -639,7 +699,7 @@ description = "ASN.1 types and codecs" name = "pyasn1" optional = false python-versions = "*" -version = "0.4.5" +version = "0.4.7" [[package]] category = "main" @@ -647,10 +707,10 @@ description = "A collection of ASN.1-based protocols modules." name = "pyasn1-modules" optional = false python-versions = "*" -version = "0.2.5" +version = "0.2.7" [package.dependencies] -pyasn1 = ">=0.4.1,<0.5.0" +pyasn1 = ">=0.4.6,<0.5.0" [[package]] category = "dev" @@ -665,7 +725,7 @@ category = "main" description = "C parser in Python" name = "pycparser" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.19" [[package]] @@ -693,7 +753,7 @@ description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.4.0" +version = "2.4.2" [[package]] category = "dev" @@ -701,10 +761,10 @@ description = "python code static checker" name = "pylint" optional = false python-versions = ">=3.5.*" -version = "2.4.0" +version = "2.4.3" [package.dependencies] -astroid = ">=2.2.0,<3" +astroid = ">=2.3.0,<2.4" colorama = "*" isort = ">=4.2.5,<5" mccabe = ">=0.6,<0.7" @@ -715,22 +775,26 @@ description = "A Pylint plugin to help Pylint understand the Django web framewor name = "pylint-django" optional = false python-versions = "*" -version = "2.0.9" +version = "2.0.11" [package.dependencies] pylint = ">=2.0" pylint-plugin-utils = ">=0.5" +[package.extras] +for_tests = ["coverage", "django-tables2", "factory-boy", "pytest"] +with_django = ["django"] + [[package]] category = "dev" description = "Utilities and helpers for writing Pylint plugins" name = "pylint-plugin-utils" optional = false python-versions = "*" -version = "0.5" +version = "0.6" [package.dependencies] -pylint = "*" +pylint = ">=1.7" [[package]] category = "main" @@ -738,7 +802,7 @@ description = "Python parsing module" name = "pyparsing" optional = true python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.0" +version = "2.4.2" [[package]] category = "main" @@ -765,7 +829,7 @@ description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" -version = "2019.1" +version = "2019.3" [[package]] category = "main" @@ -802,6 +866,10 @@ chardet = ">=3.0.2,<3.1.0" idna = ">=2.5,<2.9" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + [[package]] category = "main" description = "Javascript Minifier" @@ -833,6 +901,11 @@ version = "0.13.0" certifi = "*" urllib3 = ">=1.9" +[package.extras] +bottle = ["bottle (>=0.12.13)"] +falcon = ["falcon (>=1.4)"] +flask = ["flask (>=0.8)", "blinker (>=1.1)"] + [[package]] category = "main" description = "Python 2 and 3 compatibility utilities" @@ -843,11 +916,11 @@ version = "1.12.0" [[package]] category = "main" -description = "This package provides 16 stemmer algorithms (15 + Poerter English stemmer) generated from Snowball algorithms." +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." name = "snowballstemmer" optional = true python-versions = "*" -version = "1.2.1" +version = "2.0.0" [[package]] category = "main" @@ -855,7 +928,7 @@ description = "A modern CSS selector implementation for Beautiful Soup." name = "soupsieve" optional = false python-versions = "*" -version = "1.9.3" +version = "1.9.4" [[package]] category = "main" @@ -884,6 +957,10 @@ sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" +[package.extras] +docs = ["sphinxcontrib-websupport"] +test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.720)", "docutils-stubs"] + [[package]] category = "main" description = "" @@ -892,6 +969,9 @@ optional = true python-versions = "*" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "main" description = "" @@ -900,6 +980,9 @@ optional = true python-versions = "*" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "main" description = "" @@ -908,6 +991,9 @@ optional = true python-versions = "*" version = "1.0.2" +[package.extras] +test = ["pytest", "flake8", "mypy", "html5lib"] + [[package]] category = "main" description = "A sphinx extension which renders display math in HTML via JavaScript" @@ -916,6 +1002,9 @@ optional = true python-versions = ">=3.5" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "main" description = "" @@ -924,6 +1013,9 @@ optional = true python-versions = "*" version = "1.0.2" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "main" description = "" @@ -932,6 +1024,9 @@ optional = true python-versions = "*" version = "1.1.3" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "main" description = "Non-validating SQL parser" @@ -946,16 +1041,16 @@ description = "The most basic Text::Unidecode port" name = "text-unidecode" optional = false python-versions = "*" -version = "1.2" +version = "1.3" [[package]] category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" -marker = "implementation_name == \"cpython\"" +marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" name = "typed-ast" optional = false python-versions = "*" -version = "1.3.5" +version = "1.4.0" [[package]] category = "main" @@ -971,7 +1066,12 @@ description = "HTTP library with thread-safe connection pooling, file post, and name = "urllib3" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.25.2" +version = "1.25.6" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] category = "main" @@ -995,89 +1095,89 @@ description = "Module for decorators, wrappers and monkey patching." name = "wrapt" optional = false python-versions = "*" -version = "1.11.1" +version = "1.11.2" [extras] docs = ["recommonmark", "sphinx"] [metadata] -content-hash = "3ed66b613497743cd6ce38a774893fb8b93eef21641ebc95a674ab58331d7268" +content-hash = "a36d202d0475f0faceccb228b2e4710039d0282bdee456de6daf9375bfe9f663" python-versions = "^3.7" [metadata.hashes] alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"] argon2-cffi = ["1029fef2f7808a89e3baa306f5ace36e768a2d847ee7b056399adcd7707f6256", "206857d870c6ca3c92514ca70a3c371be47383f7ae6a448f5a16aa17baa550ba", "3558a7e22b886efad0c99b23b9be24880213b4e2d1630095459978cfcae570e2", "457fd6de741859aa91c750ffad97f12675c4356047e43392c5fb21f5d9f48b24", "4a1daa9f6960cdbdb865efcabac4158693459f52e7582c9f8a7c92dc61cdc8e1", "4bfb603184ea678563c0f1f1872367e81a3d2b70646a627d38ccede68d7b9194", "5d7493ed10e384b84b6dac862fe96c443297a25b991a8364d94a67b6cd1e9569", "5fb080047517add8d27baeb38a314814b5ab9c72630606788909b3f60a8f054a", "7453b16496b5629005a43c5f5707ef8a31fcfa5bb0ed34b5ba7b86a3cc9d02f2", "81548a27b919861040cb928a350733f4f9455dd67c7d1ba92eb5960a1d7f8b26", "84fd768d523f87097d572cdfb98e868cdbdc8e80e3d444787fd32e7f6ae25b02", "8b4cf6c0298f33b92fcd50f19899175b7421690fc8bc6ac68368320c158cbf51", "af6a4799411eee3f7133fead973727f5fefacd18ea23f51039e70cae51ceb109", "df7d60a4cf58dc08319fedc0506b42ec0fa5221c6e1f9e2e89fcddff92507390", "f9072e9f70185a57e36228d34aad4bb644e6a8b4fd6a45f856c666f38f6de96c", "fbae1d08b52f9a791500c650ab51ba00e374eaeccb5dbaa41b99dab4fd4115e8", "fe91e3bd95aeae70366693dcc970db03a71619d19df6fbaabf662c3b3c54cdf8", "fec86ee6f913154846171f66ee30c893c0cde3d434911f8b31c1f84a9aea410e"] -astroid = ["6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", "b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"] -babel = ["6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", "8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"] -bcrypt = ["0ba875eb67b011add6d8c5b76afbd92166e98b1f1efab9433d5dc0fafc76e203", "21ed446054c93e209434148ef0b362432bb82bbdaf7beef70a32c221f3e33d1c", "28a0459381a8021f57230954b9e9a65bb5e3d569d2c253c5cac6cb181d71cf23", "2aed3091eb6f51c26b7c2fad08d6620d1c35839e7a362f706015b41bd991125e", "2fa5d1e438958ea90eaedbf8082c2ceb1a684b4f6c75a3800c6ec1e18ebef96f", "3a73f45484e9874252002793518da060fb11eaa76c30713faa12115db17d1430", "3e489787638a36bb466cd66780e15715494b6d6905ffdbaede94440d6d8e7dba", "44636759d222baa62806bbceb20e96f75a015a6381690d1bc2eda91c01ec02ea", "678c21b2fecaa72a1eded0cf12351b153615520637efcadc09ecf81b871f1596", "75460c2c3786977ea9768d6c9d8957ba31b5fbeb0aae67a5c0e96aab4155f18c", "8ac06fb3e6aacb0a95b56eba735c0b64df49651c6ceb1ad1cf01ba75070d567f", "8fdced50a8b646fff8fa0e4b1c5fd940ecc844b43d1da5a980cb07f2d1b1132f", "9b2c5b640a2da533b0ab5f148d87fb9989bf9bcb2e61eea6a729102a6d36aef9", "a9083e7fa9adb1a4de5ac15f9097eb15b04e2c8f97618f1b881af40abce382e1", "b7e3948b8b1a81c5a99d41da5fb2dc03ddb93b5f96fcd3fd27e643f91efa33e1", "b998b8ca979d906085f6a5d84f7b5459e5e94a13fc27c28a3514437013b6c2f6", "dd08c50bc6f7be69cd7ba0769acca28c846ec46b7a8ddc2acf4b9ac6f8a7457e", "de5badee458544ab8125e63e39afeedfcf3aef6a6e2282ac159c95ae7472d773", "ede2a87333d24f55a4a7338a6ccdccf3eaa9bed081d1737e0db4dbd1a4f7e6b6"] -beautifulsoup4 = ["05668158c7b85b791c5abde53e50265e16f98ad601c402ba44d70f96c4159612", "25288c9e176f354bf277c0a10aa96c782a6a18a17122dba2e8cec4a97e03343b", "f040590be10520f2ea4c2ae8c3dae441c7cfff5308ec9d58a0ec0c1b8f81d469"] +astroid = ["09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", "5a79c9b4bd6c4be777424593f957c996e20beb5f74e0bc332f47713c6f675efe"] +babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"] +bcrypt = ["0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", "0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", "19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", "5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", "69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", "6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", "74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", "763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", "8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", "9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", "a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", "a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", "c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", "cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", "d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", "ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"] +beautifulsoup4 = ["5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169", "6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931", "dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57"] bleach = ["213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", "3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"] cachecontrol = ["cef77effdf51b43178f6a2d3b787e3734f98ade253fa3187f3bb7315aaa42ff7"] -cachetools = ["219b7dc6024195b6f2bc3d3f884d1fef458745cd323b04165378622dcc823852", "9efcc9fab3b49ab833475702b55edd5ae07af1af7a4c627678980b45e459c460"] -certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] -cffi = ["041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", "046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", "066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", "066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", "2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", "300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", "34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", "46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", "4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", "4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", "4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", "50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", "55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", "5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", "59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", "73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", "a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", "a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", "a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", "a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", "ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", "b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", "d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", "d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", "dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", "e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", "e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", "ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"] +cachetools = ["428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae", "8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a"] +certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"] +cffi = ["08f99e8b38d5134d504aa7e486af8e4fde66a2f388bbecc270cdd1e00fa09ff8", "1112d2fc92a867a6103bce6740a549e74b1d320cf28875609f6e93857eee4f2d", "1b9ab50c74e075bd2ae489853c5f7f592160b379df53b7f72befcbe145475a36", "24eff2997436b6156c2f30bed215c782b1d8fd8c6a704206053c79af95962e45", "2eff642fbc9877a6449026ad66bf37c73bf4232505fb557168ba5c502f95999b", "362e896cea1249ed5c2a81cf6477fabd9e1a5088aa7ea08358a4c6b0998294d2", "40eddb3589f382cb950f2dcf1c39c9b8d7bd5af20665ce273815b0d24635008b", "5ed40760976f6b8613d4a0db5e423673ca162d4ed6c9ed92d1f4e58a47ee01b5", "632c6112c1e914c486f06cfe3f0cc507f44aa1e00ebf732cedb5719e6aa0466a", "64d84f0145e181f4e6cc942088603c8db3ae23485c37eeda71cb3900b5e67cb4", "6cb4edcf87d0e7f5bdc7e5c1a0756fbb37081b2181293c5fdf203347df1cd2a2", "6f19c9df4785305669335b934c852133faed913c0faa63056248168966f7a7d5", "719537b4c5cd5218f0f47826dd705fb7a21d83824920088c4214794457113f3f", "7b0e337a70e58f1a36fb483fd63880c9e74f1db5c532b4082bceac83df1523fa", "853376efeeb8a4ae49a737d5d30f5db8cdf01d9319695719c4af126488df5a6a", "85bbf77ffd12985d76a69d2feb449e35ecdcb4fc54a5f087d2bd54158ae5bb0c", "8978115c6f0b0ce5880bc21c967c65058be8a15f1b81aa5fdbdcbea0e03952d1", "8f7eec920bc83692231d7306b3e311586c2e340db2dc734c43c37fbf9c981d24", "8fe230f612c18af1df6f348d02d682fe2c28ca0a6c3856c99599cdacae7cf226", "92068ebc494b5f9826b822cec6569f1f47b9a446a3fef477e1d11d7fac9ea895", "b57e1c8bcdd7340e9c9d09613b5e7fdd0c600be142f04e2cc1cc8cb7c0b43529", "ba956c9b44646bc1852db715b4a252e52a8f5a4009b57f1dac48ba3203a7bde1", "ca42034c11eb447497ea0e7b855d87ccc2aebc1e253c22e7d276b8599c112a27", "dc9b2003e9a62bbe0c84a04c61b0329e86fccd85134a78d7aca373bbbf788165", "dd308802beb4b2961af8f037becbdf01a1e85009fdfc14088614c1b3c383fae5", "e77cd105b19b8cd721d101687fcf665fd1553eb7b57556a1ef0d453b6fc42faa", "f56dff1bd81022f1c980754ec721fb8da56192b026f17f0f99b965da5ab4fbd2", "fa4cc13c03ea1d0d37ce8528e0ecc988d2365e8ac64d8d86cafab4038cb4ce89", "fa8cf1cb974a9f5911d2a0303f6adc40625c05578d8e7ff5d313e1e27850bd59", "fb003019f06d5fc0aa4738492ad8df1fa343b8a37cbcf634018ad78575d185df", "fd409b7778167c3bcc836484a8f49c0e0b93d3e745d975749f83aa5d18a5822f", "fe5d65a3ee38122003245a82303d11ac05ff36531a8f5ce4bc7d4bbc012797e1"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] -commonmark = ["14c3df31e8c9c463377e287b2a1eefaa6019ab97b22dad36e2f32be59d61d68d", "867fc5db078ede373ab811e16b6789e9d033b15ccd7296f370ca52d1ee792ce0"] -coverage = ["0c5fe441b9cfdab64719f24e9684502a59432df7570521563d7b1aff27ac755f", "2b412abc4c7d6e019ce7c27cbc229783035eef6d5401695dccba80f481be4eb3", "3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", "39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", "3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", "42692db854d13c6c5e9541b6ffe0fe921fe16c9c446358d642ccae1462582d3b", "465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", "48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", "4ec30ade438d1711562f3786bea33a9da6107414aed60a5daa974d50a8c2c351", "5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", "5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", "6899797ac384b239ce1926f3cb86ffc19996f6fa3a1efbb23cb49e0c12d8c18c", "68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", "6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", "7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", "7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", "839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", "8e679d1bde5e2de4a909efb071f14b472a678b788904440779d2c449c0355b27", "8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", "932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", "93f965415cc51604f571e491f280cff0f5be35895b4eb5e55b47ae90c02a497b", "988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", "998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", "9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", "9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", "a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", "a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", "a9abc8c480e103dc05d9b332c6cc9fb1586330356fc14f1aa9c0ca5745097d19", "aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", "bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", "bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", "c22ab9f96cbaff05c6a84e20ec856383d27eae09e511d3e6ac4479489195861d", "c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", "c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", "c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", "ca58eba39c68010d7e87a823f22a081b5290e3e3c64714aac3c91481d8b34d22", "df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", "f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", "f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", "f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", "fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a"] -django = ["6fcc3cbd55b16f9a01f37de8bcbe286e0ea22e87096557f1511051780338eaea", "bb407d0bb46395ca1241f829f5bd03f7e482f97f7d1936e26e98dacb201ed4ec"] +commonmark = ["452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", "da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"] +coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] +django = ["4025317ca01f75fc79250ff7262a06d8ba97cd4f82e93394b2a0a6a4a925caeb", "a8ca1033acac9f33995eb2209a6bf18a4681c3e5269a878e9a7e0b7384ed1ca3"] django-appconf = ["35f13ca4d567f132b960e2cd4c832c2d03cb6543452d34e29b7ba10371ba80e3", "c98a7af40062e996b921f5962a1c4f3f0c979fa7885f7be4710cceb90ebe13a6"] -django-bootstrap4 = ["c7d7f89e59eb5b702da0fd44438094f20e8f174970e64afece124f38cb4f3aee"] +django-bootstrap4 = ["3da770392819267eda2f774bcf832460af00db21089b94caf4df94be8a48c48c"] django-compressor = ["7732676cfb9d58498dfb522b036f75f3f253f72ea1345ac036434fdc418c2e57", "9616570e5b08e92fa9eadc7a1b1b49639cce07ef392fc27c74230ab08075b30f"] django-ical = ["80071168c7113d8ddf5907bd02bc32af017cf2bfb401d3e988d657819bce4756", "afdf3020e6f7ed5955a4fc4d500283714b80bed16fcc3724a24e1fdae2bff7ed"] django-libsass = ["49db3334b87e1f7955c4f9fb9945bc296f8bfd27a14d6d89706e4b0e5dc5de1c"] django-localflavor = ["0cee94c4b8f0214a5ba7be7e935019a8c062f4e7726d1df4b1e453cb812b2039", "12ce98b13adcd68bb4babcd937d0ae5a0fd5801f71acaf9a6bf1784c218ef53c"] -django-sendfile2 = ["b1654d844d68da45620bc27eda3c4b89c2cbbd521146f88a05f3347375807757"] +django-sendfile2 = ["267cdd817a5fe7e649df9139ac3efbe8675c61ccdab43146d1e8cbd9bab70554"] django-template-check = ["72c424239c09ae76782e4357b29a34cacfcb0f64a8fabb478f6526c84507e3d2", "9a543d5e4c5db541c4e4c23b3139324caa494b52614e1c02febc93e8ce2fc395"] django-tinymce4-lite = ["e6776bc5b2c7237705fea18668574bc1c4dff36babc90c99a2bb7b5d636eb5e8", "f0958117ddacc72596e80746729e02a727264413ab54b799f3b697a44e054e87"] -djangorestframework = ["1f274ebbb930dfa709751fac31b2332b2e662303f8c66d7eacea534702611e34", "bd389598d052fb79a68904eff35d34303118b5c2abefa65329b5f1a07b571ce1"] -docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] +djangorestframework = ["5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8", "dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090"] +docutils = ["6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] factory-boy = ["728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee", "faf48d608a1735f0d0a3c9cbf536d64f9132b547dae7ba452c4d99a79e84a370"] -faker = ["96ad7902706f2409a2d0c3de5132f69b413555a419bacec99d3f16e657895b47", "b3bb64aff9571510de6812df45122b633dbc6227e870edae3ed9430f94698521"] +faker = ["5902379d8df308a204fc11c4f621590ee83975805a6c7b2228203b9defa45250", "5e8c755c619f332d5ec28b7586389665f136bcf528e165eb925e87c06a63eda7"] firebase-admin = ["37a6cd13f981499db6033f326edf321a86024cab725151b33548f1f56f7bde03", "df58e1d24803f518178c2f7108f48b31f632845839455cb4349d0d6316529021"] -flake8 = ["859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", "a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"] -freezegun = ["6cb82b276f83f2acce67f121dc2656f4df26c71e32238334eb071170b892a278", "e839b43bfbe8158b4d62bb97e6313d39f3586daf48e1314fb1083d2ef17700da"] -future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] -google-api-core = ["2c23fbc81c76b941ffb71301bb975ed66a610e9b03f918feacd1ed59cf43a6ec", "b2b91107bcc3b981633c89602b46451f6474973089febab3ee51c49cb7ae6a1f"] -google-api-python-client = ["048da0d68564380ee23b449e5a67d4666af1b3b536d2fb0a02cee1ad540fa5ec", "5def5a485b1cbc998b8f869456c7bde0c0e6d3d0a5ea1f300b5ef57cb4b1ce8f"] +flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] +freezegun = ["2a4d9c8cd3c04a201e20c313caf8b6338f1cfa4cda43f46a94cc4a9fd13ea5e7", "edfdf5bc6040969e6ed2e36eafe277963bdc8b7c01daeda96c5c8594576c9390"] +google-api-core = ["b95895a9398026bc0500cf9b4a3f82c3f72c3f9150b26ff53af40c74e91c264a", "df8adc4b97f5ab4328a0e745bee77877cf4a7d4601cb1cd5959d2bbf8fba57aa"] +google-api-python-client = ["3121d55d106ef1a2756e8074239512055bd99eb44da417b3dd680f9a1385adec", "a8a88174f66d92aed7ebbd73744c2c319b4b1ce828e565f9ec721352d2e2fb8c"] google-auth = ["0f7c6a64927d34c1a474da92cfc59e552a5d3b940d3266606c6a28b72888b9e4", "20705f6803fd2c4d1cc2dcb0df09d4dfcb9a7d51fd59e94a3a28231fd93119ed"] google-auth-httplib2 = ["098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445", "f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08"] google-cloud-core = ["0ee17abc74ff02176bee221d4896a00a3c202f3fb07125a7d814ccabd20d7eb5", "10750207c1a9ad6f6e082d91dbff3920443bdaf1c344a782730489a9efa802f1"] -google-cloud-firestore = ["b67732bd61124d15b2e6c857128ce2a6945ad3a312300de5f152ec0a0d85ff5d", "ba6fcf2f6c60a1275bcbb42ee716d7dd061cd9dea0540060b17134a6f73b0ce0"] -google-cloud-storage = ["1e1fce811753bd2570db3d468437d44052274641c7f637c50e1c2df16497b9f7", "21f94c073eb9be34ca545c4eef28ae66947386687d99cd3a82400bf1504cfbd8"] -google-resumable-media = ["2dae98ee716efe799db3578a7b902fbf5592fc5c77d3c0906fc4ef9b1b930861", "3e38923493ca0d7de0ad91c31acfefc393c78586db89364e91cb4f11990e51ba"] +google-cloud-firestore = ["201fa86bbc76cf7ccbfac293bb7ed2dfba9bb9e5244b2785f619d083a8b2b51d", "5634bea05c7206f15b1de547d2315b2f483e6be0501d9aa413f7929d3703344b"] +google-cloud-storage = ["13a6a820311662eb91a99810568c2bca5ddc7e44e2163fed4cb3f4d47da132cf", "2e7e2435978bda1c209b70a9a00b8cbc53c3b00d6f09eb2c991ebba857babf24"] +google-resumable-media = ["5fd2e641f477e50be925a55bcfdf0b0cb97c2b92aacd7b15c1d339f70d55c1c7", "cdeb8fbb3551a665db921023603af2f0d6ac59ad8b48259cb510b8799505775f"] googleapis-common-protos = ["e61b8ed5e36b976b487c6e7b15f31bb10c7a0ca7bd5c0e837f4afab64b53a0c6"] -httplib2 = ["23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8", "a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600"] +grpcio = ["0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", "0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503", "0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff", "0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb", "0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7", "0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208", "11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55", "16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d", "1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408", "2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a", "3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1", "41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b", "4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f", "44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6", "53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b", "6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b", "6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6", "73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407", "785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b", "82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc", "86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950", "95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049", "96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900", "970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9", "ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695", "ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855", "b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4", "c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1", "d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6", "d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5", "e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22", "e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909"] +httplib2 = ["34537dcdd5e0f2386d29e0e2c6d4a1703a3b982d34c198a5102e6e5d6194b107", "409fa5509298f739b34d5a652df762cb0042507dc93f6633e306b11289d6249d"] icalendar = ["07c2447a1d44cbb27c90b8c6a5c98e890cc1853c6223e2a52195cddec26c6356", "83f7248b7485ddd29c7d69b706b21c441e34855d9c1d888939fd24aefdd9d19b"] idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] -isort = ["c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a", "f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d"] -jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] +isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"] +jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] jsmin = ["b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b"] -lazy-object-proxy = ["159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", "23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", "3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", "3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", "4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", "4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", "64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", "6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", "7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", "7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", "8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", "a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", "acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", "be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", "bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", "c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", "dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", "e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1"] -libsass = ["000de439948e001934f697c51c025a6842e43c212974b6a2d5218f057114f5be", "049097611c246364dcad1e44986a3bb565e39ddffe64fb3956570161a995020e", "1813dadccbe22afe56da7ce495db19117b59c50c13688fbcc1b92070b94d33ed", "25b88650f3f13fcd5d5e5d3b4c12c6e77136acd36cf0c26ef4b1bd4eac71bd70", "2d2630bc6fa94106cd69a72c46c10e3f7378922aed988a058ca8fc672765fac7", "53570a2505794c191de1484a9c407359cd12f797a3ef50e474a95f66915a51c4", "69c86e98fb11ab74356dc054be61ae1ce6882b0dece5be409bb5ba7b1b696885", "703bf25e5065bc8f45154fe828eee24ab7e14660088c2784e09741aecab118f9", "7179a3862fb8e2cc5d1f7e97fed0a2afed49141df1949f43fbf7c9516c48d064", "8ac0910a43758097c816bdf9f7af85e564aff49c64ef9571be036cb599fe510a", "ac1188c967b7c4aff23be32974c688cd55b8d9771ee6c988b39e557508c3b3f2", "b1890110043c11d11640d97334946975d7f43494dc46076c50fb5c7701878cab", "b7215262984cc3f692e705564ab5405841a51cfca1408062ae9776ef4b4dabff", "becb4c41f7edda34312d27787bcff4c9363a4f877bf87476dd863a0d7cf7bbef", "dc75c1ed7f7863f9c23009002713fcc75331f50eb7e32e7aa1e21cc5d17071f1"] +lazy-object-proxy = ["02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", "18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", "1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", "2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", "616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", "63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", "77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", "83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", "84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", "874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", "9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", "a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", "a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", "ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", "ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", "b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", "c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", "fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1"] +libsass = ["3113ef32eaf3662c162c250db6883d7a5f177856bfd8bb632a147cb0a95e4fee", "312d135e6bd1a137927fed781dab497c05930305265e3d3b1da3b3d916cd97a6", "32f8322aad9b6b864b826adb5e193d704d5fb2c816f85a5cc5bf775730e5d024", "4252e24c8869d6ce764052f200445331d1881b5c2d283d6131a30d0684b10403", "517324814f81cd2642cb1e9fd772e8e50e336c7c8833d50535a731e5b4c84606", "607ce32c3b31542e0bf1bc2409627dd7247a3849ba720ec34d23426b96346199", "6124594e72ba216b00131795ad5ea5de1e0cf8784e63a01e0c6a4e4c13fc7914", "6129063002fc8337b734f5963ac3eb01ead51e9c88c6d27e73ddc9236cb15b2e", "75b38c236be6ca03e3dd3789f3044180fc0836b7c9e4991fcc52a8570f47dc91", "9c711d4e4d003fec7f98fe87bb1faf7d88e6d648356413d8b8d9d76bd1844089", "b15a0e61bd54764e658bc6931015453fa34d954f87c3b6fd35624e13bcacf69d", "c22cdc37121b730e5fb87bc8d3eee8c4b1fe219a04d198a535fbd22895c99e27", "c5ba74babfb3a6976611312e0026c4668913cdf05e009921e1f54146ccdc02a4"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] -msgpack = ["26cb40116111c232bc235ce131cc3b4e76549088cb154e66a2eb8ff6fcc907ec", "300fd3f2c664a3bf473d6a952f843b4a71454f4c592ed7e74a36b205c1782d28", "3129c355342853007de4a2a86e75eab966119733eb15748819b6554363d4e85c", "31f6d645ee5a97d59d3263fab9e6be76f69fa131cddc0d94091a3c8aca30d67a", "3ce7ef7ee2546c3903ca8c934d09250531b80c6127e6478781ae31ed835aac4c", "4008c72f5ef2b7936447dcb83db41d97e9791c83221be13d5e19db0796df1972", "62bd8e43d204580308d477a157b78d3fee2fb4c15d32578108dc5d89866036c8", "70cebfe08fb32f83051971264466eadf183101e335d8107b80002e632f425511", "72cb7cf85e9df5251abd7b61a1af1fb77add15f40fa7328e924a9c0b6bc7a533", "7c55649965c35eb32c499d17dadfb8f53358b961582846e1bc06f66b9bccc556", "86b963a5de11336ec26bc4f839327673c9796b398b9f1fe6bb6150c2a5d00f0f", "8c73c9bcdfb526247c5e4f4f6cf581b9bb86b388df82cfcaffde0a6e7bf3b43a", "8e68c76c6aff4849089962d25346d6784d38e02baa23ffa513cf46be72e3a540", "97ac6b867a8f63debc64f44efdc695109d541ecc361ee2dce2c8884ab37360a1", "9d4f546af72aa001241d74a79caec278bcc007b4bcde4099994732e98012c858", "a28e69fe5468c9f5251c7e4e7232286d71b7dfadc74f312006ebe984433e9746", "fd509d4aa95404ce8d86b4e32ce66d5d706fd6646c205e1c2a715d87078683a2"] -packaging = ["0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", "9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"] +msgpack = ["0cc7ca04e575ba34fea7cfcd76039f55def570e6950e4155a4174368142c8e1b", "187794cd1eb73acccd528247e3565f6760bd842d7dc299241f830024a7dd5610", "1904b7cb65342d0998b75908304a03cb004c63ef31e16c8c43fee6b989d7f0d7", "229a0ccdc39e9b6c6d1033cd8aecd9c296823b6c87f0de3943c59b8bc7c64bee", "24149a75643aeaa81ece4259084d11b792308a6cf74e796cbb35def94c89a25a", "30b88c47e0cdb6062daed88ca283b0d84fa0d2ad6c273aa0788152a1c643e408", "32fea0ea3cd1ef820286863a6202dcfd62a539b8ec3edcbdff76068a8c2cc6ce", "355f7fd0f90134229eaeefaee3cf42e0afc8518e8f3cd4b25f541a7104dcb8f9", "4abdb88a9b67e64810fb54b0c24a1fd76b12297b4f7a1467d85a14dd8367191a", "757bd71a9b89e4f1db0622af4436d403e742506dbea978eba566815dc65ec895", "76df51492bc6fa6cc8b65d09efdb67cbba3cbfe55004c3afc81352af92b4a43c", "774f5edc3475917cd95fe593e625d23d8580f9b48b570d8853d06cac171cd170", "8a3ada8401736df2bf497f65589293a86c56e197a80ae7634ec2c3150a2f5082", "a06efd0482a1942aad209a6c18321b5e22d64eb531ea20af138b28172d8f35ba", "b24afc52e18dccc8c175de07c1d680bdf315844566f4952b5bedb908894bec79", "b8b4bd3dafc7b92608ae5462add1c8cc881851c2d4f5d8977fdea5b081d17f21", "c6e5024fc0cdf7f83b6624850309ddd7e06c48a75fa0d1c5173de4d93300eb19", "db7ff14abc73577b0bcbcf73ecff97d3580ecaa0fc8724babce21fdf3fe08ef6", "dedf54d72d9e7b6d043c244c8213fe2b8bbfe66874b9a65b39c4cc892dd99dd4", "ea3c2f859346fcd55fc46e96885301d9c2f7a36d453f5d8f2967840efa1e1830", "f0f47bafe9c9b8ed03e19a100a743662dd8c6d0135e684feea720a0d0046d116"] +packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] pillow = ["00fdeb23820f30e43bba78eb9abb00b7a937a655de7760b2e09101d63708b64e", "01f948e8220c85eae1aa1a7f8edddcec193918f933fb07aaebe0bfbbcffefbf1", "08abf39948d4b5017a137be58f1a52b7101700431f0777bec3d897c3949f74e6", "099a61618b145ecb50c6f279666bbc398e189b8bc97544ae32b8fcb49ad6b830", "2c1c61546e73de62747e65807d2cc4980c395d4c5600ecb1f47a650c6fa78c79", "2ed9c4f694861642401f27dc3cb99772be67cd190e84845c749dae0a06c3bfae", "338581b30b908e111be578f0297255f6b57a51358cd16fa0e6f664c9a1f88bff", "38c7d48a21cd06fdeee93987147b9b1c55b73b4cfcbf83240568bfbd5adee447", "43fd026f613c8e48a25eba1a92f4d2ad7f3903c95d8c33a11611a7717d2ab654", "4548236844327a718ce3bb182ab32a16fa2050c61e334e959f554cac052fb0df", "5090857876c58885cfa388dc649e5db30aae98a068c26f3fd0ac9d7d9a4d9572", "5bbba34f97a26a93f5e8dec469ca4ddd712451418add43da946dbaed7f7a98d2", "65a28969a025a0eb4594637b6103201dc4ed2a9508bdab56ac33e43e3081c404", "892bb52b70bd5ea9dbbc3ac44f38e84f5a04e9d8b1bff48159d96cb795b81159", "8a9becd5cbd5062f973bcd2e7bc79483af310222de112b6541f8af1f93a3cc42", "972a7aaeb7c4a2795b52eef52ee991ef040b31009f36deca6207a986607b55f3", "97b119c436bfa96a92ac2ca525f7025836d4d4e64b1c9f9eff8dbaf3ff1d86f3", "9ba37698e242223f8053cc158f130aee046a96feacbeab65893dbe94f5530118", "b1b0e1f626a0f079c0d3696db70132fb1f29aa87c66aecb6501a9b8be64ce9f7", "c14c1224fd1a5be2733530d648a316974dbbb3c946913562c6005a76f21ca042", "c79a8546c48ae6465189e54e3245a97ddf21161e33ff7eaa42787353417bb2b6", "ceb76935ac4ebdf6d7bc845482a4450b284c6ccfb281e34da51d510658ab34d8", "e22bffaad04b4d16e1c091baed7f2733fc1ebb91e0c602abf1b6834d17158b1f", "ec883b8e44d877bda6f94a36313a1c6063f8b1997aa091628ae2f34c7f97c8d5", "f1baa54d50ec031d1a9beb89974108f8f2c0706f49798f4777df879df0e1adb6", "f53a5385932cda1e2c862d89460992911a89768c65d176ff8c50cddca4d29bed"] -protobuf = ["21e395d7959551e759d604940a115c51c6347d90a475c9baf471a1a86b5604a9", "57e05e16955aee9e6a0389fcbd58d8289dd2420e47df1a1096b3a232c26eb2dd", "67819e8e48a74c68d87f25cad9f40edfe2faf278cdba5ca73173211b9213b8c9", "75da7d43a2c8a13b0bc7238ab3c8ae217cbfd5979d33b01e98e1f78defb2d060", "78e08371e236f193ce947712c072542ff19d0043ab5318c2ea46bbc2aaebdca6", "7ee5b595db5abb0096e8c4755e69c20dfad38b2d0bcc9bc7bafc652d2496b471", "86260ecfe7a66c0e9d82d2c61f86a14aa974d340d159b829b26f35f710f615db", "92c77db4bd33ea4ee5f15152a835273f2338a5246b2cbb84bab5d0d7f6e9ba94", "9c7b90943e0e188394b4f068926a759e3b4f63738190d1ab3d500d53b9ce7614", "a77f217ea50b2542bae5b318f7acee50d9fc8c95dd6d3656eaeff646f7cab5ee", "ad589ed1d1f83db22df867b10e01fe445516a5a4d7cfa37fe3590a5f6cfc508b", "b06a794901bf573f4b2af87e6139e5cd36ac7c91ac85d7ae3fe5b5f6fc317513", "bd8592cc5f8b4371d0bad92543370d4658dc41a5ccaaf105597eb5524c616291", "be48e5a6248a928ec43adf2bea037073e5da692c0b3c10b34f9904793bd63138", "cc5eb13f5ccc4b1b642cc147c2cdd121a34278b341c7a4d79e91182fff425836", "cd3b0e0ad69b74ee55e7c321f52a98effed2b4f4cc9a10f3683d869de00590d5", "d6e88c4920660aa75c0c2c4b53407aef5efd9a6e0ca7d2fc84d79aba2ccbda3a", "ec3c49b6d247152e19110c3a53d9bb4cf917747882017f70796460728b02722e", "f1f5d8b8e0bc9651d81b40ad3d9fb7cdd858ea31fc116dd230393465849dbecd"] -psycopg2-binary = ["007ca0df127b1862fc010125bc4100b7a630efc6841047bd11afceadb4754611", "03c49e02adf0b4d68f422fdbd98f7a7c547beb27e99a75ed02298f85cb48406a", "0a1232cdd314e08848825edda06600455ad2a7adaa463ebfb12ece2d09f3370e", "131c80d0958c89273d9720b9adf9df1d7600bb3120e16019a7389ab15b079af5", "2de34cc3b775724623f86617d2601308083176a495f5b2efc2bbb0da154f483a", "2eddc31500f73544a2a54123d4c4b249c3c711d31e64deddb0890982ea37397a", "484f6c62bdc166ee0e5be3aa831120423bf399786d1f3b0304526c86180fbc0b", "4c2d9369ed40b4a44a8ccd6bc3a7db6272b8314812d2d1091f95c4c836d92e06", "70f570b5fa44413b9f30dbc053d17ef3ce6a4100147a10822f8662e58d473656", "7a2b5b095f3bd733aab101c89c0e1a3f0dfb4ebdc26f6374805c086ffe29d5b2", "804914a669186e2843c1f7fbe12b55aad1b36d40a28274abe6027deffad9433d", "8520c03172da18345d012949a53617a963e0191ccb3c666f23276d5326af27b5", "90da901fc33ea393fc644607e4a3916b509387e9339ec6ebc7bfded45b7a0ae9", "a582416ad123291a82c300d1d872bdc4136d69ad0b41d57dc5ca3df7ef8e3088", "ac8c5e20309f4989c296d62cac20ee456b69c41fd1bc03829e27de23b6fa9dd0", "b2cf82f55a619879f8557fdaae5cec7a294fac815e0087c4f67026fdf5259844", "b59d6f8cfca2983d8fdbe457bf95d2192f7b7efdb2b483bf5fa4e8981b04e8b2", "be08168197021d669b9964bd87628fa88f910b1be31e7010901070f2540c05fd", "be0f952f1c365061041bad16e27e224e29615d4eb1fb5b7e7760a1d3d12b90b6", "c1c9a33e46d7c12b9c96cf2d4349d783e3127163fd96254dcd44663cf0a1d438", "d18c89957ac57dd2a2724ecfe9a759912d776f96ecabba23acb9ecbf5c731035", "d7e7b0ff21f39433c50397e60bf0995d078802c591ca3b8d99857ea18a7496ee", "da0929b2bf0d1f365345e5eb940d8713c1d516312e010135b14402e2a3d2404d", "de24a4962e361c512d3e528ded6c7480eab24c655b8ca1f0b761d3b3650d2f07", "e45f93ff3f7dae2202248cf413a87aeb330821bf76998b3cf374eda2fc893dd7", "f046aeae1f7a845041b8661bb7a52449202b6c5d3fb59eb4724e7ca088811904", "f1dc2b7b2748084b890f5d05b65a47cd03188824890e9a60818721fd492249fb", "fcbe7cf3a786572b73d2cd5f34ed452a5f5fac47c9c9d1e0642c457a148f9f88"] -pyasn1 = ["061442c60842f6d11051d4fdae9bc197b64bd41573a12234a753a0cb80b4f30b", "0ee2449bf4c4e535823acc25624c45a8b454f328d59d3f3eeb82d3567100b9bd", "5f9fb05c33e53b9a6ee3b1ed1d292043f83df465852bec876e93b47fd2df7eed", "65201d28e081f690a32401e6253cca4449ccacc8f3988e811fae66bd822910ee", "79b336b073a52fa3c3d8728e78fa56b7d03138ef59f44084de5f39650265b5ff", "8ec20f61483764de281e0b4aba7d12716189700debcfa9e7935780850bf527f3", "9458d0273f95d035de4c0d5e0643f25daba330582cc71bb554fe6969c015042a", "98d97a1833a29ca61cd04a60414def8f02f406d732f9f0bcb49f769faff1b699", "b00d7bfb6603517e189d1ad76967c7e805139f63e43096e5f871d1277f50aea5", "b06c0cfd708b806ea025426aace45551f91ea7f557e0c2d4fbd9a4b346873ce0", "d14d05984581770333731690f5453efd4b82e1e5d824a1d7976b868a2e5c38e8", "da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", "da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"] -pyasn1-modules = ["230730c6e63d283df75459b1b791d73648f801fd46ffcc9eb1abd16c67dfa3a6", "27f09b212203f820bc982937bd41952e856610dbd7c48d9366e8e63a551824c8", "490ed2974883c6e3d0ee53f53b32427f29ea030345c11d690788d1ed31ed666b", "49663b587853cd8783427d2fd115c862916bdd3c01656a8110ecd1950699e28f", "4df864e4dd01e600ffe280191a6630bb5b86d46382c1bcec4d03a700cb35c8b9", "6eeef742c31e285c23ebef32d8e0fee5e4ee1a563bb5171684621165b7e65627", "d1c66c80615ee74b1f3867d31b14e81f5f961a0e1afe5429838f21b5065d0161", "d7aa971a8cd79482ec5ae98705b54fdfaf834c24ed93ebc83f422c7700412b47", "e573fcf31e72c2ede48a58c8559fe9083cd007623c99a3eaf0c8f5719c09a2f8", "e980f089e3ec8116d6a5154c80f002ca941ad3446b5048a5b6d225f24ded85bb", "ef721f68f7951fab9b0404d42590f479e30d9005daccb1699b0a51bb4177db96", "f01c6899938f635b2ff4d158e760625416e20f03c612cfc9da7e97798c84e916", "f309b6c94724aeaf7ca583feb1cc70430e10d7551de5e36edfc1ae6909bcfb3c"] +protobuf = ["125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", "1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", "27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", "3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", "3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", "45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", "56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", "5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", "6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", "6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", "84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", "b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", "ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", "cc9af00df3fc9302f537a8335668c20be27916b2277e9a5eaed510266e2bb33b", "db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", "f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", "f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de"] +psycopg2-binary = ["080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809", "110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598", "171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5", "19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1", "249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d", "2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e", "2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00", "2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf", "3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43", "3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5", "51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70", "52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6", "5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd", "63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877", "8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3", "85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67", "8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68", "908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b", "af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a", "af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b", "b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2", "cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e", "d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e", "d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f", "dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f", "e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7", "f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737", "f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7"] +pyasn1 = ["1321d4b2f051410fe7302bb1619903d30b24ba1451d019c11d242d11b2a35444", "2860a047f666afd23b197a65f33145313511c368ce919b2d9b1853ffd3e9d32d", "2919babd43b3b44247c23201b71072c0c65a636daa595cad5bcd276094dbfc2d", "437a23121602c0bb6c65320b27e31e334ffd73a9ca5c6c075b66b6270b1a8184", "5a89df3c62688261e27439d5715fd0d3ca6bf7bf1067e2171642e92aff17e817", "62cdade8b5530f0b185e09855dd422bc05c0bbff6b72ff61381c09dac7befd8c", "67a43aec85f4ea96e72a7b22227ba7a45cf03b7297e1a53418be164bbf68335e", "813b198c169e9442f340743f77093435bf3e1de8d1731f3abc45d44afba17556", "96c44b5604e7674e53e27fce98f3fc68821d9546151b98842c27b533122649da", "a9495356ca1d66ed197a0f72b41eb1823cf7ea8b5bd07191673e8147aecf8604", "bcac468e38d16e94fee4c8f76eef1feb9a06a911e93465f2351a4140fa66d303", "c39d11c72f0e5e71faa35c8c8ef5ee9b810ec99a3c64f05133f1325fe5636bba", "f124185ccc1c1c5e782aa58d46bc28be279673a482334d70de6735d05d8b4b10"] +pyasn1-modules = ["0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b", "13a6955947d8a554de78fc305a4d651f20fb5580b88612a5f0661d4f189d27ac", "233f55c840e821e76828262db976ac894b285909d22d060c2bdb522e7bf28cc6", "24d54188cb7abd750e0a2cba61b7b46a75608175a0c3c1b1eee08322915d8d21", "27581362b4253b9c999882a64df974124cde12be0bf2c04148a0d68bc6bbb7b8", "33c220a2701032261a23eea6e9881404ac6fc7ff96f183b5353fea8fc8962547", "64f6aecf26e93f6a3ba3725b4eb9f532551747d7a63ca9ff43aef12f4bf11eac", "7b4edf07ca2f759d7cf693184be09f22e067c2eb52b03c770d0a2e9de1c67dfd", "9b972f81f59d896cebb9ebb1d44296f1acb28bf7869443c37551f4eed8d74f83", "9ca5e376a6d9dee35bb3a62608dfa2e6698798aa6b8db3c7afd0eb31af0d63c7", "b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3", "c14b107a67ee36a7f183ae9f4803ffde4a03b67f3192eab0a62e851af71371d3", "eaf35047a0b068e3e0c2a99618b13b65c98c329661daa78c9d44a4ef0fe8139e"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"] pydenticon = ["2ef363cdd6f4f0193ce62257486027e36884570f6140bbde51de72df321b77f1"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] -pygments = ["31cba6ffb739f099a85e243eff8cb717089fdd3c7300767d9fc34cb8e1b065f5", "5ad302949b3c98dd73f8d9fcdc7e9cb592f120e32a18e23efd7f3dc51194472b"] -pylint = ["92280a6085fc5e4fec67d6330c0c85eae50817696d02bdc85e9ca6bab830ad58", "ef796b99c243afeebf7a04b4426126ac837940da6bcd5fc47229c507e056fec1"] -pylint-django = ["c0562562bffbdc97a26d007d818231348633282bec66ba445540a036a0ae76f5", "e4abeef83f6f6577951ca0b2d12f73fc0c53dd33272fee4982c8cb42e4ae64ad"] -pylint-plugin-utils = ["8d9e31d5ea8b7b0003e1f0f136b44a5235896a32e47c5bc2ef1143e9f6ba0b74"] -pyparsing = ["1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", "9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"] +pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] +pylint = ["7b76045426c650d2b0f02fc47c14d7934d17898779da95288a74c2a7ec440702", "856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0"] +pylint-django = ["6740b60dd94b6896cffa28d7fe1bb5dd167cbdcb3d9fb3db01b30390ea18b987", "e1273decd5ee60eaf9a742f62dd67bacb7ccf2cb1e7641dba0361108d0dff455"] +pylint-plugin-utils = ["2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", "57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"] +pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] python-magic = ["f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375", "f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5"] -pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"] +pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"] rcssmin = ["ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"] recommonmark = ["29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb", "2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852"] requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] @@ -1085,8 +1185,8 @@ rjsmin = ["dd9591aa73500b08b7db24367f8d32c6470021f39d5ab4e50c7c02e4401386f1"] rsa = ["14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66", "1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487"] sentry-sdk = ["7d8668f082cb1eb9bf1e0d3f8f9bd5796d05d927c1197af226d044ed32b9815f", "ff14935cc3053de0650128f124c36f34a4be120b8cc522c149f5cba342c1fd05"] six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] -snowballstemmer = ["919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", "9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"] -soupsieve = ["8662843366b8d8779dec4e2f921bebec9afd856a5ff2e82cd419acc5054a1a92", "a5a6166b4767725fd52ae55fee8c8b6137d9a51e9f1edea461a062a759160118"] +snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"] +soupsieve = ["605f89ad5fdbfefe30cdc293303665eff2d188865d4dbe4eb510bba1edfbfce3", "b91d676b330a0ebd5b21719cb6e9b57c57d433671f65b9c28dd3461d9a1ed0b6"] sphinx = ["0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845", "839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"] sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"] sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"] @@ -1095,10 +1195,10 @@ sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"] sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"] sqlparse = ["40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", "7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"] -text-unidecode = ["5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d", "801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"] -typed-ast = ["132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b", "18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d", "2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a", "3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462", "4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee", "4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a", "5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4", "6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649", "7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a", "8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f", "8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7", "912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760", "b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18", "c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616", "c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd", "ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21", "eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93", "f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb", "f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7"] +text-unidecode = ["1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", "bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"] +typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] uritemplate = ["01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", "1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", "c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"] -urllib3 = ["a53063d8b9210a7bdec15e7b272776b9d42b2fd6816401a0d43006ad2f9902db", "d363e3607d8de0c220d31950a8f38b18d5ba7c0830facd71a1c6b1036b7ce06c"] +urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"] uwsgi = ["4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583"] webencodings = ["a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", "b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"] -wrapt = ["4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"] +wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] diff --git a/pyproject.toml b/pyproject.toml index a69c0566f5f0369f9fdbb929777b39ba5a053196..9de1c1271d541c11b4ce05ab0be9280f6d94be29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ django-sendfile2 = "~0.4.2" # docs requirements recommonmark = { version = "~0.6.0", optional = true } sphinx = { version = "~2.2", optional = true } +google-api-python-client = "^1.7.11" [tool.poetry.extras] docs = ["recommonmark", "sphinx"] diff --git a/website/events/tests/test_models.py b/website/events/tests/test_models.py index 83a2487cb4dfb454019751576aa7b48accc26fa6..a30c6794c87a296ac2fa77dee4ce3aaa3c05d874 100644 --- a/website/events/tests/test_models.py +++ b/website/events/tests/test_models.py @@ -1,6 +1,8 @@ import datetime +import factory from django.core.exceptions import ValidationError +from django.db.models import signals from django.test import TestCase from django.utils import timezone @@ -16,6 +18,7 @@ class EventTest(TestCase): fixtures = ['members.json'] @classmethod + @factory.django.mute_signals(signals.pre_save) def setUpTestData(cls): cls.mailinglist = MailingList.objects.create( name="testmail" diff --git a/website/events/tests/test_views.py b/website/events/tests/test_views.py index ada0818fe050d454a23828c673376c27a2e94832..b3630e67a0c6b0b34b2ff96f4d216683cd75e89c 100644 --- a/website/events/tests/test_views.py +++ b/website/events/tests/test_views.py @@ -1,7 +1,9 @@ import datetime +import factory from django.contrib.auth.models import Permission from django.core import mail +from django.db.models import signals from django.test import Client, TestCase from django.utils import timezone @@ -134,6 +136,7 @@ class RegistrationTest(TestCase): fixtures = ['members.json', 'member_groups.json'] @classmethod + @factory.django.mute_signals(signals.pre_save) def setUpTestData(cls): cls.mailinglist = MailingList.objects.create( name="testmail" diff --git a/website/mailinglists/admin.py b/website/mailinglists/admin.py index f96dd4ce17ae02e9cf4279e7c799097ea0a90889..8a5ff244cd9a016b3babd707558487927a5bc6e8 100644 --- a/website/mailinglists/admin.py +++ b/website/mailinglists/admin.py @@ -28,5 +28,5 @@ class MailingListAdmin(admin.ModelAdmin): def alias_names(self, obj): """Return list of aliases of obj.""" - return [x.alias for x in obj.aliasses.all()] - alias_names.short_description = _('List aliasses') + return [x.alias for x in obj.aliases.all()] + alias_names.short_description = _('List aliases') diff --git a/website/mailinglists/api/serializers.py b/website/mailinglists/api/serializers.py index fa899936a53706a6e404af36a981062fccc353c3..2177ee0fbf85093d43b5f6b8835c70db7615c89b 100644 --- a/website/mailinglists/api/serializers.py +++ b/website/mailinglists/api/serializers.py @@ -19,7 +19,7 @@ class MailingListSerializer(serializers.ModelSerializer): def _names(self, instance): """Return list of names of the the mailing list and its aliases.""" - return [instance.name] + [x.alias for x in instance.aliasses.all()] + return [instance.name] + [x.alias for x in instance.aliases.all()] def _addresses(self, instance): """Return list of all subscribed addresses.""" diff --git a/website/mailinglists/apps.py b/website/mailinglists/apps.py index f54aac34b38d2ee5710d3c57f668b4344dce8479..223413838d37e5e30b40438837101d590ae5cae0 100644 --- a/website/mailinglists/apps.py +++ b/website/mailinglists/apps.py @@ -8,3 +8,7 @@ class MailinglistsConfig(AppConfig): name = 'mailinglists' verbose_name = _('Mailing lists') + + def ready(self): + """Imports the signals when the app is ready""" + from . import signals # noqa: F401 diff --git a/website/mailinglists/gsuite.py b/website/mailinglists/gsuite.py new file mode 100644 index 0000000000000000000000000000000000000000..11e8000ad0d78388139d2e4c1b6318335ae45249 --- /dev/null +++ b/website/mailinglists/gsuite.py @@ -0,0 +1,326 @@ +"""GSuite syncing helpers defined by the mailinglists package""" +import logging +import threading +from time import sleep +from typing import List + +from django.conf import settings +from django.utils.datastructures import ImmutableList +from googleapiclient.errors import HttpError + +from mailinglists.models import MailingList +from mailinglists.services import get_automatic_lists +from utils.google_api import get_directory_api, get_groups_settings_api + +logger = logging.getLogger(__name__) +logger.setLevel(logging.WARNING) + + +class GSuiteSyncService: + class GroupData: + def __init__(self, name, description='', moderated=False, + aliases=ImmutableList([]), addresses=ImmutableList([])): + super().__init__() + self.moderated = moderated + self.name = name + self.description = description + self.aliases = aliases + self.addresses = addresses + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.__dict__ == other.__dict__ + return False + + def __init__(self, groups_settings_api=get_groups_settings_api(), + directory_api=get_directory_api()): + super().__init__() + self.groups_settings_api = groups_settings_api + self.directory_api = directory_api + + @staticmethod + def _group_settings(moderated): + return { + 'allowExternalMembers': 'true', + 'allowWebPosting': 'false', + 'archiveOnly': 'false', + 'isArchived': 'true', + 'membersCanPostAsTheGroup': 'false', + 'messageModerationLevel': 'MODERATE_ALL_MESSAGES' + if moderated else 'MODERATE_NONE', + 'replyTo': 'REPLY_TO_SENDER', + 'whoCanAssistContent': 'NONE', + 'whoCanContactOwner': 'ALL_MANAGERS_CAN_CONTACT', + 'whoCanDiscoverGroup': 'ALL_MEMBERS_CAN_DISCOVER', + 'whoCanJoin': 'INVITED_CAN_JOIN', + 'whoCanLeaveGroup': 'NONE_CAN_LEAVE', + 'whoCanModerateContent': 'OWNERS_AND_MANAGERS', + 'whoCanModerateMembers': 'NONE', + 'whoCanPostMessage': 'ANYONE_CAN_POST', + 'whoCanViewGroup': 'ALL_MANAGERS_CAN_VIEW', + 'whoCanViewMembership': 'ALL_MANAGERS_CAN_VIEW' + } + + def create_group(self, group): + """ + Create a new group based on the provided data + :param group: group data + """ + try: + self.directory_api.groups().insert( + body={ + 'email': f'{group.name}@{settings.GSUITE_DOMAIN}', + 'name': group.name, + 'description': group.description, + }, + ).execute() + # Wait for mailinglist creation to complete Docs say we need to + # wait a minute, but since we always update lists + # an error in the list members update is not a problem + sleep(0.5) + self.groups_settings_api.groups().update( + groupUniqueId=f'{group.name}@{settings.GSUITE_DOMAIN}', + body=self._group_settings(group.moderated) + ).execute() + except HttpError as e: + logger.error(f'Could not successfully finish ' + f'creating the list {group.name}', e.content) + return + + self._update_group_members(group) + self._update_group_aliases(group) + + def update_group(self, old_name, group): + """ + Update a group based on the provided name and data + :param old_name: old group name + :param group: new group data + """ + try: + self.directory_api.groups().update( + groupKey=f'{old_name}@{settings.GSUITE_DOMAIN}', + body={ + 'email': f'{group.name}@{settings.GSUITE_DOMAIN}', + 'name': group.name, + 'description': group.description, + } + ).execute() + self.groups_settings_api.groups().update( + groupUniqueId=f'{group.name}@{settings.GSUITE_DOMAIN}', + body=self._group_settings(group.moderated) + ).execute() + except HttpError as e: + logger.error(f'Could not update list {group.name}', e.content) + return + + self._update_group_members(group) + self._update_group_aliases(group) + + def _update_group_aliases(self, group: GroupData): + """ + Update the aliases of a group based on existing values + :param group: group data + """ + try: + aliases_response = self.directory_api.groups().aliases().list( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + ).execute() + except HttpError as e: + logger.error(f'Could not obtain existing aliases ' + f'for list {group.name}', e.content) + return + + existing_aliases = [a['alias'] for a in + aliases_response.get('aliases', [])] + new_aliases = [f'{a}@{settings.GSUITE_DOMAIN}' for a in group.aliases] + + remove_list = [x for x in existing_aliases if x not in new_aliases] + insert_list = [x for x in new_aliases if x not in existing_aliases] + + for remove_alias in remove_list: + try: + self.directory_api.groups().aliases().delete( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + alias=remove_alias + ).execute() + except HttpError as e: + logger.error( + f'Could not remove alias ' + f'{remove_alias} for list {group.name}', + e.content + ) + + for insert_alias in insert_list: + try: + self.directory_api.groups().aliases().insert( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + body={ + 'alias': insert_alias + } + ).execute() + except HttpError as e: + logger.error( + f'Could not insert alias ' + f'{insert_alias} for list {group.name}', + e.content + ) + + def delete_group(self, name: str): + """ + Set the specified list to unused, this is not a real delete + :param name: Group name + """ + try: + self.groups_settings_api.groups().patch( + groupUniqueId=f'{name}@{settings.GSUITE_DOMAIN}', + body={ + 'archiveOnly': 'true', + 'whoCanPostMessage': 'NONE_CAN_POST' + } + ).execute() + self._update_group_members( + GSuiteSyncService.GroupData(name, addresses=[]) + ) + self._update_group_aliases( + GSuiteSyncService.GroupData(name, aliases=[]) + ) + except HttpError as e: + logger.error(f'Could not delete list {name}', e.content) + + def _update_group_members(self, group: GroupData): + """ + Update the group members of the specified group based + on the existing members + :param group: group data + """ + try: + members_response = self.directory_api.members().list( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + ).execute() + members_list = members_response.get('members', []) + while 'nextPageToken' in members_response: + members_response = self.directory_api.members().list( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + pageToken=members_response['nextPageToken'] + ).execute() + members_list += members_response.get('members', []) + + existing_members = [m['email'] for m in members_list + if m['role'] == 'MEMBER'] + existing_managers = [m['email'] for m in members_list + if m['role'] == 'MANAGER'] + except HttpError as e: + logger.error('Could not obtain list member data', e.content) + return # the list does not exist or something else is wrong + new_members = list(group.addresses) + + remove_list = [x for x in existing_members if x not in new_members] + insert_list = [x for x in new_members if x not in existing_members + and x not in existing_managers] + + for remove_member in remove_list: + try: + self.directory_api.members().delete( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + memberKey=remove_member + ).execute() + except HttpError as e: + logger.error(f'Could not remove list member ' + f'{remove_member} from {group.name}', e.content) + + for insert_member in insert_list: + try: + self.directory_api.members().insert( + groupKey=f'{group.name}@{settings.GSUITE_DOMAIN}', + body={ + 'email': insert_member, + 'role': 'MEMBER' + } + ).execute() + except HttpError as e: + logger.error(f'Could not insert list member ' + f'{insert_member} in {group.name}', e.content) + + @staticmethod + def mailinglist_to_group(mailinglist: MailingList): + """Convert a mailinglist model to everything we need for GSuite""" + return GSuiteSyncService.GroupData( + moderated=mailinglist.moderated, + name=mailinglist.name, + description=mailinglist.description, + aliases=[x.alias for x in mailinglist.aliases.all()], + addresses=list(mailinglist.all_addresses()) + ) + + @staticmethod + def _automatic_to_group(automatic_list): + """Convert an automatic mailinglist to a GSuite Group data obj""" + return GSuiteSyncService.GroupData( + moderated=automatic_list['moderated'], + name=automatic_list['name'], + description=automatic_list['description'], + aliases=automatic_list['aliases'], + addresses=automatic_list['addresses'] + ) + + def _get_default_lists(self): + return [self.mailinglist_to_group(l) for l in + MailingList.objects.all() + ] + [self._automatic_to_group(l) for l in + get_automatic_lists() + ] + + def sync_mailinglists(self, lists: List[GroupData] = None): + """ + Sync mailing lists with GSuite + :param lists: optional parameter to determine which lists to sync + """ + if lists is None: + lists = self._get_default_lists() + + try: + groups_response = self.directory_api.groups().list( + domain=settings.GSUITE_DOMAIN + ).execute() + groups_list = groups_response.get('groups', []) + while 'nextPageToken' in groups_response: + groups_response = self.directory_api.groups().list( + domain=settings.GSUITE_DOMAIN, + pageToken=groups_response['nextPageToken'] + ).execute() + groups_list += groups_response.get('groups', []) + existing_groups = [g['name'] for g in groups_list + if int(g['directMembersCount']) > 0] + archived_groups = [g['name'] for g in groups_list + if g['directMembersCount'] == '0'] + except HttpError as e: + logger.error('Could not get the existing groups', e.content) + return # there are no groups or something went wrong + + new_groups = [g.name for g in lists if len(g.addresses) > 0] + + remove_list = [x for x in existing_groups if x not in new_groups] + insert_list = [x for x in new_groups if x not in existing_groups] + + threads = [] + + for l in lists: + if l.name in insert_list and l.name not in archived_groups: + thread = threading.Thread(target=self.create_group, + args=(l,)) + threads.append(thread) + thread.start() + elif len(l.addresses) > 0: + thread = threading.Thread(target=self.update_group, + args=(l.name, l)) + threads.append(thread) + thread.start() + + for l in remove_list: + thread = threading.Thread(target=self.delete_group, + args=(l,)) + threads.append(thread) + thread.start() + + for th in threads: + th.join() diff --git a/website/mailinglists/management/commands/sync_mailinglists.py b/website/mailinglists/management/commands/sync_mailinglists.py new file mode 100644 index 0000000000000000000000000000000000000000..61039faf8d47182b1433a4a70f92354ba8d4762c --- /dev/null +++ b/website/mailinglists/management/commands/sync_mailinglists.py @@ -0,0 +1,16 @@ +"""Mailing list syncing management command""" +import logging + +from django.core.management.base import BaseCommand + +from mailinglists.gsuite import GSuiteSyncService + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + + def handle(self, *args, **options): + """Sync all mailing lists""" + sync_service = GSuiteSyncService() + sync_service.sync_mailinglists() diff --git a/website/mailinglists/migrations/0015_auto_20191013_2352.py b/website/mailinglists/migrations/0015_auto_20191013_2352.py new file mode 100644 index 0000000000000000000000000000000000000000..af15975a0dba65857134ca888f654f3156eb0ef4 --- /dev/null +++ b/website/mailinglists/migrations/0015_auto_20191013_2352.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.1 on 2019-10-13 21:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailinglists', '0014_mailinglist_description'), + ] + + operations = [ + migrations.AlterModelOptions( + name='listalias', + options={'verbose_name': 'List alias', 'verbose_name_plural': 'List aliases'}, + ), + migrations.RemoveField( + model_name='mailinglist', + name='autoresponse_enabled', + ), + migrations.RemoveField( + model_name='mailinglist', + name='autoresponse_text', + ), + migrations.RemoveField( + model_name='mailinglist', + name='prefix', + ), + migrations.RemoveField( + model_name='mailinglist', + name='archived', + ), + migrations.AlterField( + model_name='listalias', + name='mailinglist', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='mailinglists.MailingList', verbose_name='Mailing list'), + ), + ] diff --git a/website/mailinglists/models.py b/website/mailinglists/models.py index fb4e38b661b6e395b59fb34e6fcd82524570b2b9..9afd0e0f12e5d6dadb122ee7dc3d1259ac36ab02 100644 --- a/website/mailinglists/models.py +++ b/website/mailinglists/models.py @@ -41,25 +41,11 @@ class MailingList(models.Model): help_text=_('Enter the name for the list (i.e. name@thalia.nu).'), ) - prefix = models.CharField( - verbose_name=_("Prefix"), - blank=True, - max_length=200, - help_text=_('Enter a prefix that should be prefixed to subjects ' - 'of all emails sent via this mailinglist.'), - ) - description = models.TextField( verbose_name=_("Description"), help_text=_('Write a description for the mailinglist.'), ) - archived = models.BooleanField( - verbose_name=_("Archived"), - default=True, - help_text=_('Indicate whether an archive should be kept.') - ) - moderated = models.BooleanField( verbose_name=_("Moderated"), default=False, @@ -80,18 +66,6 @@ class MailingList(models.Model): blank=True, ) - autoresponse_enabled = models.BooleanField( - verbose_name=_("Automatic response enabled"), - default=False, - help_text=_('Indicate whether emails will get an automatic response.') - ) - - autoresponse_text = models.TextField( - verbose_name=_("Autoresponse text"), - null=True, - blank=True, - ) - def all_addresses(self): """Return all addresses subscribed to this mailing list.""" for member in self.members.all(): @@ -119,11 +93,6 @@ class MailingList(models.Model): } }) - if not self.autoresponse_text and self.autoresponse_enabled: - raise ValidationError({ - 'autoresponse_text': _('Enter a text for the auto response.') - }) - def __str__(self): """Return the name of the mailing list.""" return self.name @@ -169,7 +138,7 @@ class ListAlias(models.Model): mailinglist = models.ForeignKey(MailingList, verbose_name=_("Mailing list"), on_delete=models.CASCADE, - related_name='aliasses') + related_name='aliases') def clean(self): """Validate the alias.""" @@ -195,4 +164,4 @@ class ListAlias(models.Model): """Meta class for ListAlias.""" verbose_name = _("List alias") - verbose_name_plural = _("List aliasses") + verbose_name_plural = _("List aliases") diff --git a/website/mailinglists/services.py b/website/mailinglists/services.py index 8bb23f6207e37e5c881d000c90b8c72da5491354..5a784ba5a69e85b162a72623c538134928a361dc 100644 --- a/website/mailinglists/services.py +++ b/website/mailinglists/services.py @@ -1,4 +1,5 @@ """The services defined by the mailinglists package""" + from django.conf import settings from django.utils import timezone @@ -39,37 +40,36 @@ def get_automatic_lists(): if 0 < timezone.now().month < 9: lectureyear += 1 active_mentorships = Mentorship.objects.filter( - year=lectureyear) + year=lectureyear).prefetch_related('member') mentors = [x.member for x in active_mentorships] lists = [] lists += _create_automatic_list( - ['leden', 'members'], '[THALIA]', - Member.all_with_membership('member'), True, True, True) + ['members', 'leden'], '[THALIA]', + Member.all_with_membership('member'), '', True, True, True) lists += _create_automatic_list( - ['begunstigers', 'benefactors'], + ['benefactors', 'begunstigers'], '[THALIA]', - Member.all_with_membership(Membership.BENEFACTOR), + Member.all_with_membership(Membership.BENEFACTOR), '', multilingual=True) lists += _create_automatic_list( - ['ereleden', 'honorary'], '[THALIA]', Member.all_with_membership( - 'honorary'), multilingual=True) + ['honorary', 'ereleden'], '[THALIA]', Member.all_with_membership( + 'honorary'), '', multilingual=True) lists += _create_automatic_list( - ['mentors'], '[THALIA] [MENTORS]', mentors, moderated=False) + ['mentors'], '[THALIA] [MENTORS]', mentors, '', moderated=False) lists += _create_automatic_list( ['activemembers'], '[THALIA] [COMMITTEES]', - active_members) + active_members, '') lists += _create_automatic_list( - ['commissievoorzitters', 'committeechairs'], '[THALIA] [CHAIRS]', - committee_chair_emails, moderated=False) + ['committeechairs', 'commissievoorzitters'], '[THALIA] [CHAIRS]', + committee_chair_emails, '', moderated=False) lists += _create_automatic_list( - ['gezelschapvoorzitters', 'societychairs'], '[THALIA] [SOCIETY]', - society_chair_emails, moderated=False) + ['societychairs', 'gezelschapvoorzitters'], '[THALIA] [SOCIETY]', + society_chair_emails, '', moderated=False) lists += _create_automatic_list( ['optin'], '[THALIA] [OPTIN]', Member.current_members.filter( - profile__receive_optin=True), - multilingual=True) + profile__receive_optin=True), '', multilingual=True) all_previous_board_members = [] @@ -97,10 +97,13 @@ def get_automatic_lists(): return lists -def _create_automatic_list(names, prefix, members, +def _create_automatic_list(names, prefix, members, description='', archived=True, moderated=True, multilingual=False): data = { 'names': names, + 'name': names[0], + 'description': description, + 'aliases': names[1:], 'prefix': prefix, 'archived': archived, 'moderated': moderated, @@ -116,6 +119,8 @@ def _create_automatic_list(names, prefix, members, if member.profile.language == language[0]] localized_data['names'] = [ '{}-{}'.format(n, language[0]) for n in names] + localized_data['name'] = localized_data['names'][0] + localized_data['aliases'] = localized_data['names'][1:] yield localized_data # these are localized lists, e.g. leden-nl@ else: data['addresses'] = set([member.email for member in members]) diff --git a/website/mailinglists/signals.py b/website/mailinglists/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..9f54fcee650bdc36802128f85dc31ad50f211f81 --- /dev/null +++ b/website/mailinglists/signals.py @@ -0,0 +1,21 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver +from googleapiclient.errors import HttpError + +from mailinglists.gsuite import GSuiteSyncService +from mailinglists.models import MailingList + + +@receiver(pre_save, sender='mailinglists.MailingList') +def pre_mailinglist_save(instance, **kwargs): + sync_service = GSuiteSyncService() + group = sync_service.mailinglist_to_group(instance) + old_list = MailingList.objects.filter(pk=instance.pk).first() + try: + if old_list is None: + sync_service.create_group(group) + else: + sync_service.update_group(old_list.name, group) + except HttpError: + # Cannot do direct create or update, do full sync for list + sync_service.sync_mailinglists([group]) diff --git a/website/mailinglists/tests/test_gsuite.py b/website/mailinglists/tests/test_gsuite.py new file mode 100644 index 0000000000000000000000000000000000000000..1b9e0f97410ae1955561a7c752faeaaaff758c21 --- /dev/null +++ b/website/mailinglists/tests/test_gsuite.py @@ -0,0 +1,467 @@ +"""Test for the GSuite sync in the mailinglists package""" +from unittest import mock +from unittest.mock import MagicMock + +import factory +from django.db.models import signals +from django.test import TestCase +from googleapiclient.errors import HttpError +from httplib2 import Response +from django.conf import settings + +from mailinglists.gsuite import GSuiteSyncService +from mailinglists.models import MailingList, ListAlias, VerbatimAddress + + +def assert_not_called_with(self, *args, **kwargs): + try: + self.assert_any_call(*args, **kwargs) + except AssertionError: + return + raise AssertionError('Expected %s to not have been called.' % + self._format_mock_call_signature(args, kwargs)) + + +MagicMock.assert_not_called_with = assert_not_called_with + + +class GSuiteSyncTestCase(TestCase): + @classmethod + @factory.django.mute_signals(signals.pre_save) + def setUpTestData(cls): + cls.settings_api = MagicMock() + cls.directory_api = MagicMock() + + cls.sync_service = GSuiteSyncService( + cls.settings_api, cls.directory_api) + cls.mailinglist = MailingList.objects.create( + name='new_group', + description='some description', + moderated=False + ) + ListAlias.objects.create( + mailinglist=cls.mailinglist, alias='alias2') + VerbatimAddress.objects.create( + mailinglist=cls.mailinglist, + address=f'test2@{settings.GSUITE_DOMAIN}' + ) + + def setUp(self): + self.settings_api.reset_mock() + self.directory_api.reset_mock() + + def test_default_lists(self): + self.assertEqual(len(self.sync_service._get_default_lists()), + 18) + + def test_automatic_to_group(self): + group = GSuiteSyncService._automatic_to_group({ + 'moderated': False, + 'name': 'new_group', + 'description': 'some description', + 'aliases': ['alias1'], + 'addresses': [f'test1@{settings.GSUITE_DOMAIN}'] + }) + self.assertEqual(group, GSuiteSyncService.GroupData( + 'new_group', 'some description', False, + ['alias1'], [f'test1@{settings.GSUITE_DOMAIN}'] + )) + + def test_mailinglist_to_group(self): + group = GSuiteSyncService.mailinglist_to_group(self.mailinglist) + self.assertEqual(group, GSuiteSyncService.GroupData( + 'new_group', 'some description', False, + ['alias2'], [f'test2@{settings.GSUITE_DOMAIN}'] + )) + + def test_group_settings(self): + self.assertEqual(self.sync_service._group_settings(False), { + 'allowExternalMembers': 'true', + 'allowWebPosting': 'false', + 'archiveOnly': 'false', + 'isArchived': 'true', + 'membersCanPostAsTheGroup': 'false', + 'messageModerationLevel': 'MODERATE_NONE', + 'replyTo': 'REPLY_TO_SENDER', + 'whoCanAssistContent': 'NONE', + 'whoCanContactOwner': 'ALL_MANAGERS_CAN_CONTACT', + 'whoCanDiscoverGroup': 'ALL_MEMBERS_CAN_DISCOVER', + 'whoCanJoin': 'INVITED_CAN_JOIN', + 'whoCanLeaveGroup': 'NONE_CAN_LEAVE', + 'whoCanModerateContent': 'OWNERS_AND_MANAGERS', + 'whoCanModerateMembers': 'NONE', + 'whoCanPostMessage': 'ANYONE_CAN_POST', + 'whoCanViewGroup': 'ALL_MANAGERS_CAN_VIEW', + 'whoCanViewMembership': 'ALL_MANAGERS_CAN_VIEW' + }) + self.assertEqual(self.sync_service._group_settings(True), { + 'allowExternalMembers': 'true', + 'allowWebPosting': 'false', + 'archiveOnly': 'false', + 'isArchived': 'true', + 'membersCanPostAsTheGroup': 'false', + 'messageModerationLevel': 'MODERATE_ALL_MESSAGES', + 'replyTo': 'REPLY_TO_SENDER', + 'whoCanAssistContent': 'NONE', + 'whoCanContactOwner': 'ALL_MANAGERS_CAN_CONTACT', + 'whoCanDiscoverGroup': 'ALL_MEMBERS_CAN_DISCOVER', + 'whoCanJoin': 'INVITED_CAN_JOIN', + 'whoCanLeaveGroup': 'NONE_CAN_LEAVE', + 'whoCanModerateContent': 'OWNERS_AND_MANAGERS', + 'whoCanModerateMembers': 'NONE', + 'whoCanPostMessage': 'ANYONE_CAN_POST', + 'whoCanViewGroup': 'ALL_MANAGERS_CAN_VIEW', + 'whoCanViewMembership': 'ALL_MANAGERS_CAN_VIEW' + }) + + @mock.patch('mailinglists.gsuite.logger') + def test_create_group(self, logger_mock): + with self.subTest('Successful'): + self.sync_service.create_group(GSuiteSyncService.GroupData( + 'new_group', 'some description', False, + ['alias2'], [f'test2@{settings.GSUITE_DOMAIN}'] + )) + + self.directory_api.groups().insert.assert_called_once_with(body={ + 'email': f'new_group@{settings.GSUITE_DOMAIN}', + 'name': 'new_group', + 'description': 'some description', + }) + + self.settings_api.groups().update.assert_called_once_with( + groupUniqueId=f'new_group@{settings.GSUITE_DOMAIN}', + body=self.sync_service._group_settings(False) + ) + + self.directory_api.members().list.assert_called() + self.directory_api.groups().aliases().list.assert_called() + + self.settings_api.reset_mock() + self.directory_api.reset_mock() + + with self.subTest('Failure'): + self.directory_api.groups().insert( + ).execute.side_effect = HttpError( + Response({'status': 500}), bytes()) + + self.sync_service.create_group(GSuiteSyncService.GroupData( + 'new_group', 'some description', False, + ['alias2'], [f'test2@{settings.GSUITE_DOMAIN}'] + )) + + self.directory_api.members().list.assert_not_called() + self.directory_api.groups().aliases().list.assert_not_called() + + logger_mock.error.assert_called_once_with( + 'Could not successfully finish ' + 'creating the list new_group', bytes() + ) + + @mock.patch('mailinglists.gsuite.logger') + def test_update_group(self, logger_mock): + with self.subTest('Successful'): + self.sync_service.update_group( + 'new_group', + GSuiteSyncService.GroupData( + 'new_group', 'some description', False, + ['alias2'], [f'test2@{settings.GSUITE_DOMAIN}'] + )) + + self.directory_api.groups().update.assert_called_once_with(body={ + 'email': f'new_group@{settings.GSUITE_DOMAIN}', + 'name': 'new_group', + 'description': 'some description', + }, groupKey=f'new_group@{settings.GSUITE_DOMAIN}') + + self.settings_api.groups().update.assert_called_once_with( + groupUniqueId=f'new_group@{settings.GSUITE_DOMAIN}', + body=self.sync_service._group_settings(False) + ) + + self.directory_api.members().list.assert_called() + self.directory_api.groups().aliases().list.assert_called() + + self.settings_api.reset_mock() + self.directory_api.reset_mock() + + with self.subTest('Failure'): + self.directory_api.groups().update( + ).execute.side_effect = HttpError( + Response({'status': 500}), bytes()) + + self.sync_service.update_group( + 'new_group', + GSuiteSyncService.GroupData( + 'new_group', 'some description', False, + ['alias2'], [f'test2@{settings.GSUITE_DOMAIN}'] + ) + ) + + self.directory_api.members().list.assert_not_called() + self.directory_api.groups().aliases().list.assert_not_called() + + logger_mock.error.assert_called_once_with( + 'Could not update list new_group', bytes() + ) + + @mock.patch('mailinglists.gsuite.logger') + def test_delete_group(self, logger_mock): + with self.subTest('Successful'): + self.sync_service.delete_group('new_group') + + self.settings_api.groups().patch.assert_called_once_with(body={ + 'archiveOnly': 'true', + 'whoCanPostMessage': 'NONE_CAN_POST' + }, groupUniqueId=f'new_group@{settings.GSUITE_DOMAIN}') + + self.directory_api.members().list.assert_called() + self.directory_api.groups().aliases().list.assert_called() + + self.settings_api.reset_mock() + self.directory_api.reset_mock() + + with self.subTest('Failure'): + self.settings_api.groups().patch( + ).execute.side_effect = HttpError( + Response({'status': 500}), bytes()) + + self.sync_service.delete_group('new_group') + + self.directory_api.members().list.assert_not_called() + self.directory_api.groups().aliases().list.assert_not_called() + + logger_mock.error.assert_called_once_with( + 'Could not delete list new_group', bytes() + ) + + @mock.patch('mailinglists.gsuite.logger') + def test_update_group_aliases(self, logger_mock): + with self.subTest('Error getting existing list'): + self.directory_api.groups( + ).aliases().list().execute.side_effect = HttpError( + Response({'status': 500}), bytes()) + self.sync_service._update_group_aliases( + GSuiteSyncService.GroupData(name='update_group')) + + logger_mock.error.assert_called_once_with( + 'Could not obtain existing aliases for list update_group', + bytes() + ) + + self.directory_api.reset_mock() + + with self.subTest('Successful with some errors'): + group_data = GSuiteSyncService.GroupData( + name='update_group', + aliases=[ + 'not_synced', + 'not_synced_error', + 'already_synced' + ] + ) + + existing_aliases = [ + {'alias': f'deleteme@{settings.GSUITE_DOMAIN}'}, + {'alias': f'deleteme_error@{settings.GSUITE_DOMAIN}'}, + {'alias': f'already_synced@{settings.GSUITE_DOMAIN}'} + ] + + self.directory_api.groups( + ).aliases().list().execute.side_effect = [{ + 'aliases': existing_aliases + }] + + self.directory_api.groups( + ).aliases().insert().execute.side_effect = [ + 'success', + HttpError(Response({'status': 500}), bytes()) + ] + + self.directory_api.groups( + ).aliases().delete().execute.side_effect = [ + 'success', + HttpError(Response({'status': 500}), bytes()) + ] + + self.sync_service._update_group_aliases(group_data) + + self.directory_api.groups().aliases().insert.assert_any_call( + groupKey=f'update_group@{settings.GSUITE_DOMAIN}', + body={ + 'alias': f'not_synced@{settings.GSUITE_DOMAIN}' + }) + + self.directory_api.groups().aliases().delete.assert_any_call( + groupKey=f'update_group@{settings.GSUITE_DOMAIN}', + alias=f'deleteme@{settings.GSUITE_DOMAIN}' + ) + + logger_mock.error.assert_any_call( + 'Could not insert alias not_synced_error@' + f'{settings.GSUITE_DOMAIN} for list update_group', + bytes() + ) + + logger_mock.error.assert_any_call( + 'Could not remove alias deleteme_error@' + f'{settings.GSUITE_DOMAIN} for list update_group', + bytes() + ) + + @mock.patch('mailinglists.gsuite.logger') + def test_update_group_members(self, logger_mock): + with self.subTest('Error getting existing list'): + self.directory_api.members().list( + ).execute.side_effect = HttpError( + Response({'status': 500}), bytes()) + self.sync_service._update_group_members( + GSuiteSyncService.GroupData(name='update_group')) + + logger_mock.error.assert_called_once_with( + 'Could not obtain list member data', + bytes() + ) + + self.directory_api.reset_mock() + + with self.subTest('Successful with some errors'): + group_data = GSuiteSyncService.GroupData( + name='update_group', + addresses=[ + 'not_synced@example.com', + 'not_synced_error@example.com', + 'already_synced@example.com' + ] + ) + + existing_aliases = [ + {'email': 'deleteme@example.com', 'role': 'MEMBER'}, + {'email': 'deleteme_error@example.com', 'role': 'MEMBER'}, + {'email': 'already_synced@example.com', 'role': 'MEMBER'}, + {'email': 'donotdelete@example.com', 'role': 'MANAGER'} + ] + + self.directory_api.members().list().execute.side_effect = [{ + 'members': existing_aliases[:1], + 'nextPageToken': 'some_token' + }, { + 'members': existing_aliases[1:] + }] + + self.directory_api.members().insert().execute.side_effect = [ + 'success', + HttpError(Response({'status': 500}), bytes()) + ] + + self.directory_api.members().delete().execute.side_effect = [ + 'success', + HttpError(Response({'status': 500}), bytes()) + ] + + self.sync_service._update_group_members(group_data) + + self.directory_api.members().insert.assert_any_call( + groupKey=f'update_group@{settings.GSUITE_DOMAIN}', + body={ + 'email': 'not_synced@example.com', + 'role': 'MEMBER' + } + ) + + self.directory_api.members().delete.assert_any_call( + groupKey=f'update_group@{settings.GSUITE_DOMAIN}', + memberKey='deleteme@example.com' + ) + + self.directory_api.members().delete.assert_not_called_with( + groupKey=f'update_group@{settings.GSUITE_DOMAIN}', + memberKey='donotdelete@example.com' + ) + + logger_mock.error.assert_any_call( + 'Could not insert list member ' + 'not_synced_error@example.com in update_group', + bytes() + ) + + logger_mock.error.assert_any_call( + 'Could not remove list member ' + 'deleteme_error@example.com from update_group', + bytes() + ) + + @mock.patch('mailinglists.gsuite.logger') + def test_sync_mailinglists(self, logger_mock): + original_create = self.sync_service.create_group + original_update = self.sync_service.update_group + original_delete = self.sync_service.delete_group + + self.sync_service.create_group = MagicMock() + self.sync_service.update_group = MagicMock() + self.sync_service.delete_group = MagicMock() + + with self.subTest('Error getting existing list'): + self.directory_api.groups().list().execute.side_effect = HttpError( + Response({'status': 500}), bytes()) + self.sync_service.sync_mailinglists() + + logger_mock.error.assert_called_once_with( + 'Could not get the existing groups', + bytes() + ) + + self.directory_api.reset_mock() + + with self.subTest('Successful'): + existing_groups = [ + {'name': 'deleteme', 'directMembersCount': '3'}, + {'name': 'already_synced', 'directMembersCount': '2'}, + {'name': 'ignore', 'directMembersCount': '0'} + ] + + self.directory_api.groups().list().execute.side_effect = [{ + 'groups': existing_groups[:1], + 'nextPageToken': 'some_token' + }, { + 'groups': existing_groups[1:] + }] + + self.sync_service.sync_mailinglists([ + GSuiteSyncService.GroupData(name='syncme', + addresses=['someone']), + GSuiteSyncService.GroupData(name='already_synced', + addresses=['someone']), + GSuiteSyncService.GroupData(name='ignore2', addresses=[]) + ]) + + self.sync_service.create_group.assert_called_with( + GSuiteSyncService.GroupData( + name='syncme', + addresses=['someone'] + )) + + self.sync_service.update_group.assert_called_with( + 'already_synced', + GSuiteSyncService.GroupData( + name='already_synced', + addresses=['someone'] + )) + + self.sync_service.delete_group.assert_called_with('deleteme') + + self.sync_service.create_group.assert_not_called_with( + GSuiteSyncService.GroupData( + name='ignore2', + addresses=[] + )) + self.sync_service.update_group.assert_not_called_with( + 'ignore2', + GSuiteSyncService.GroupData( + name='ignore2', + addresses=[] + )) + self.sync_service.delete_group.assert_not_called_with('ignore2') + + self.sync_service.create_group = original_create + self.sync_service.update_group = original_update + self.sync_service.delete_group = original_delete diff --git a/website/mailinglists/tests/test_models.py b/website/mailinglists/tests/test_models.py index 68d642125666e87647ddc66706b475a823a8fc3f..c41f9a9c612359d8ef7e0bbb2309eddf130bb892 100644 --- a/website/mailinglists/tests/test_models.py +++ b/website/mailinglists/tests/test_models.py @@ -1,5 +1,7 @@ """Tests for models in the mailinglists package""" +import factory from django.core.exceptions import ValidationError +from django.db.models import signals from django.test import TestCase from mailinglists.models import MailingList, ListAlias @@ -9,6 +11,7 @@ class MailingListTest(TestCase): """Tests mailing lists""" @classmethod + @factory.django.mute_signals(signals.pre_save) def setUpTestData(cls): cls.mailinglist = MailingList.objects.create( name="mailtest", @@ -43,20 +46,12 @@ class MailingListTest(TestCase): mailinglist.name = "activemembers1" mailinglist.clean() - def test_autoresponse_has_text(self): - self.mailinglist.autoresponse_enabled = True - - with self.assertRaises(ValidationError): - self.mailinglist.clean() - - self.mailinglist.autoresponse_text = "Hello World" - self.mailinglist.clean() - class ListAliasTest(TestCase): """Tests list aliases""" @classmethod + @factory.django.mute_signals(signals.pre_save) def setUpTestData(cls): cls.mailinglist = MailingList.objects.create( name="mailtest", diff --git a/website/thaliawebsite/settings/__init__.py b/website/thaliawebsite/settings/__init__.py index 50fc3b058fe7d8c9387693f18c778b1d5bb79112..b27032c997b724acc8916eea9b29afc57376cd40 100644 --- a/website/thaliawebsite/settings/__init__.py +++ b/website/thaliawebsite/settings/__init__.py @@ -9,6 +9,8 @@ overrides. # flake8: noqa: ignore F403 import logging from firebase_admin import initialize_app, credentials +from googleapiclient.discovery import build +from google.oauth2 import service_account # Load all default settings because we need to use settings.configure # for sphinx documentation generation. @@ -39,3 +41,11 @@ if FIREBASE_CREDENTIALS != {}: credential=credentials.Certificate(FIREBASE_CREDENTIALS)) except ValueError as e: logger.error('Firebase application failed to initialise') + + +if GSUITE_ADMIN_CREDENTIALS != {}: + GSUITE_ADMIN_CREDENTIALS = ( + service_account.Credentials.from_service_account_info( + GSUITE_ADMIN_CREDENTIALS, scopes=GSUITE_ADMIN_SCOPES + ).with_subject(GSUITE_ADMIN_USER) + ) diff --git a/website/thaliawebsite/settings/production.py b/website/thaliawebsite/settings/production.py index b25213c9187ff9571207067f135b8113a2e58468..d760d45af3e22ed079dc84d6242eef2733fec4a7 100644 --- a/website/thaliawebsite/settings/production.py +++ b/website/thaliawebsite/settings/production.py @@ -78,6 +78,14 @@ if not (FIREBASE_CREDENTIALS == '{}'): FIREBASE_CREDENTIALS = base64.urlsafe_b64decode(FIREBASE_CREDENTIALS) FIREBASE_CREDENTIALS = json.loads(FIREBASE_CREDENTIALS) +GSUITE_ADMIN_CREDENTIALS = os.environ.get('GSUITE_ADMIN_CREDENTIALS', '{}') +if not (GSUITE_ADMIN_CREDENTIALS == '{}'): + GSUITE_ADMIN_CREDENTIALS = base64.urlsafe_b64decode( + GSUITE_ADMIN_CREDENTIALS) +GSUITE_ADMIN_CREDENTIALS = json.loads(GSUITE_ADMIN_CREDENTIALS) +GSUITE_ADMIN_USER = os.environ.get('GSUITE_ADMIN_USER', None) +GSUITE_DOMAIN = os.environ.get('GSUITE_DOMAIN', 'thalia.nu') + if os.environ.get('DJANGO_SSLONLY'): SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True diff --git a/website/thaliawebsite/settings/settings.py b/website/thaliawebsite/settings/settings.py index d6eb697831b9f6a525d527367cb5550066091acc..206fa417dc6ca57e2427a17748597855b41dbe50 100644 --- a/website/thaliawebsite/settings/settings.py +++ b/website/thaliawebsite/settings/settings.py @@ -261,6 +261,16 @@ THUMBNAIL_SIZES = { # Placeholder Firebase config FIREBASE_CREDENTIALS = {} +# Placeholder GSuite config +GSUITE_ADMIN_CREDENTIALS = {} +GSUITE_ADMIN_USER = 'concrexit@thalia.nu' +GSUITE_ADMIN_SCOPES = [ + 'https://www.googleapis.com/auth/admin.directory.group', + 'https://www.googleapis.com/auth/admin.directory.user', + 'https://www.googleapis.com/auth/apps.groups.settings' +] +GSUITE_DOMAIN = 'thalia.localhost' + # Default FROM email DEFAULT_FROM_EMAIL = f'noreply@{SITE_DOMAIN}' SERVER_EMAIL = DEFAULT_FROM_EMAIL diff --git a/website/utils/google_api.py b/website/utils/google_api.py new file mode 100644 index 0000000000000000000000000000000000000000..f908372fd2ca537733d9cda395320194d0ca466d --- /dev/null +++ b/website/utils/google_api.py @@ -0,0 +1,33 @@ +from googleapiclient.discovery import build +from googleapiclient.discovery_cache.base import Cache + +from thaliawebsite import settings + + +class MemoryCache(Cache): + _CACHE = {} + + def get(self, url): + return MemoryCache._CACHE.get(url) + + def set(self, url, content): + MemoryCache._CACHE[url] = content + + +memory_cache = MemoryCache() + + +def get_directory_api(): + return build( + 'admin', 'directory_v1', + credentials=settings.GSUITE_ADMIN_CREDENTIALS, + cache=memory_cache + ) + + +def get_groups_settings_api(): + return build( + 'groupssettings', 'v1', + credentials=settings.GSUITE_ADMIN_CREDENTIALS, + cache=memory_cache + )