diff --git a/CHANGES.rst b/CHANGES.rst index a82f7cfb..5c00a8b8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Added - Implemented account expiration based on OpenLDAP ppolicy overlay. Needs OpenLDAP 2.5+ :issue:`13` :pr:`118` +- Timezone configuration entry. :issue:`137` :pr:`130` Fixed ***** diff --git a/canaille/app/forms.py b/canaille/app/forms.py index e124acbe..d2f6631b 100644 --- a/canaille/app/forms.py +++ b/canaille/app/forms.py @@ -1,8 +1,11 @@ +import datetime import math +import pytz import wtforms from canaille.app.i18n import DEFAULT_LANGUAGE_CODE from canaille.app.i18n import locale_selector +from canaille.app.i18n import timezone_selector from flask import abort from flask import current_app from flask import make_response @@ -81,3 +84,31 @@ class TableForm(I18NFormMixin, FlaskForm): def validate_page(self, field): if field.data < 1 or field.data > self.page_max: raise wtforms.validators.ValidationError(_("The page number is not valid")) + + +class DateTimeUTCField(wtforms.DateTimeLocalField): + def _value(self): + if not self.data: + return "" + + user_timezone = timezone_selector() + locale_dt = self.data.astimezone(user_timezone) + return locale_dt.strftime(self.format[0]) + + def process_formdata(self, valuelist): + if not valuelist: + return + + date_str = " ".join(valuelist) + user_timezone = timezone_selector() + for format in self.strptime_format: + try: + unaware_dt = datetime.datetime.strptime(date_str, format) + locale_dt = user_timezone.localize(unaware_dt) + utc_dt = locale_dt.astimezone(pytz.utc) + self.data = utc_dt + return + except ValueError: + self.data = None + + raise ValueError(self.gettext("Not a valid datetime value.")) diff --git a/canaille/app/i18n.py b/canaille/app/i18n.py index 0fe3f358..3d1d68bd 100644 --- a/canaille/app/i18n.py +++ b/canaille/app/i18n.py @@ -1,6 +1,8 @@ import gettext import pycountry +import pytz +from babel.dates import LOCALTZ from flask import current_app from flask import g from flask import request @@ -13,7 +15,9 @@ babel = Babel() def setup_i18n(app): - babel.init_app(app, locale_selector=locale_selector) + babel.init_app( + app, locale_selector=locale_selector, timezone_selector=timezone_selector + ) @app.before_request def before_request(): @@ -40,6 +44,13 @@ def locale_selector(): return request.accept_languages.best_match(available_language_codes) +def timezone_selector(): + try: + return pytz.timezone(current_app.config.get("TIMEZONE")) + except pytz.exceptions.UnknownTimeZoneError: + return LOCALTZ + + def native_language_name_from_code(code): language = pycountry.languages.get(alpha_2=code[:2]) if code == DEFAULT_LANGUAGE_CODE: diff --git a/canaille/conf/config.sample.toml b/canaille/conf/config.sample.toml index 1e664501..cf456642 100644 --- a/canaille/conf/config.sample.toml +++ b/canaille/conf/config.sample.toml @@ -25,6 +25,10 @@ SECRET_KEY = "change me before you go in production" # If unset, language is detected # LANGUAGE = "en" +# The timezone in which datetimes will be displayed to the users. +# If unset, the server timezone will be used. +# TIMEZONE = UTC + # If you have a sentry instance, you can set its dsn here: # SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0" diff --git a/canaille/core/forms.py b/canaille/core/forms.py index f9bd3bf2..8c2b13cb 100644 --- a/canaille/core/forms.py +++ b/canaille/core/forms.py @@ -1,5 +1,6 @@ import wtforms.form from canaille.app import models +from canaille.app.forms import DateTimeUTCField from canaille.app.forms import HTMXBaseForm from canaille.app.forms import HTMXForm from canaille.app.forms import is_uri @@ -286,7 +287,7 @@ def profile_form(write_field_names, readonly_field_names, user=None): del fields["groups"] if current_app.backend.get().has_account_lockability(): # pragma: no branch - fields["lock_date"] = wtforms.DateTimeLocalField( + fields["lock_date"] = DateTimeUTCField( _("Account expiration"), validators=[wtforms.validators.Optional()], format=[ diff --git a/demo/conf-docker/canaille.toml b/demo/conf-docker/canaille.toml index f0128d10..da818e4f 100644 --- a/demo/conf-docker/canaille.toml +++ b/demo/conf-docker/canaille.toml @@ -25,6 +25,10 @@ FAVICON = "/static/img/canaille-c.png" # If unset, language is detected # LANGUAGE = "en" +# The timezone in which datetimes will be displayed to the users. +# If unset, the server timezone will be used. +# TIMEZONE = UTC + # If you have a sentry instance, you can set its dsn here: # SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0" diff --git a/demo/conf/canaille.toml b/demo/conf/canaille.toml index 0fdf647a..97627be6 100644 --- a/demo/conf/canaille.toml +++ b/demo/conf/canaille.toml @@ -25,6 +25,10 @@ FAVICON = "/static/img/canaille-c.png" # If unset, language is detected # LANGUAGE = "en" +# The timezone in which datetimes will be displayed to the users. +# If unset, the server timezone will be used. +# TIMEZONE = UTC + # If you have a sentry instance, you can set its dsn here: # SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0" diff --git a/doc/configuration.rst b/doc/configuration.rst index 8c79008f..db099848 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -34,6 +34,9 @@ Canaille is based on Flask, so any `flask configuration =1.1.1)", "pympler", "pyte name = "authlib" version = "1.2.0" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -category = "main" optional = false python-versions = "*" files = [ @@ -82,7 +77,6 @@ cryptography = ">=3.2" name = "babel" version = "2.12.1" description = "Internationalization utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -97,7 +91,6 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" -category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -116,7 +109,6 @@ lxml = ["lxml"] name = "blinker" version = "1.6.2" description = "Fast, simple object-to-object and broadcast signaling" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -128,7 +120,6 @@ files = [ name = "certifi" version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -140,7 +131,6 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -217,7 +207,6 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -229,7 +218,6 @@ files = [ name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -314,7 +302,6 @@ files = [ name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -330,7 +317,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -340,63 +326,62 @@ files = [ [[package]] name = "coverage" -version = "7.2.5" +version = "7.2.6" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"}, - {file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"}, - {file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"}, - {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"}, - {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"}, - {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"}, - {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"}, - {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"}, - {file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"}, - {file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"}, - {file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"}, - {file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"}, - {file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"}, - {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"}, - {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"}, - {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"}, - {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"}, - {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"}, - {file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"}, - {file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"}, - {file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"}, - {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"}, - {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"}, - {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"}, - {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"}, - {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"}, - {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"}, - {file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"}, - {file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"}, - {file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"}, - {file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"}, - {file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"}, - {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"}, - {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"}, - {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"}, - {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"}, - {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"}, - {file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"}, - {file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"}, - {file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"}, - {file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"}, - {file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"}, - {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"}, - {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"}, - {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"}, - {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"}, - {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"}, - {file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"}, - {file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"}, - {file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"}, - {file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"}, + {file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"}, + {file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"}, + {file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"}, + {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"}, + {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"}, + {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"}, + {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"}, + {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"}, + {file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"}, + {file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"}, + {file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"}, + {file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"}, + {file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"}, + {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"}, + {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"}, + {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"}, + {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"}, + {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"}, + {file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"}, + {file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"}, + {file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"}, + {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"}, + {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"}, + {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"}, + {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"}, + {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"}, + {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"}, + {file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"}, + {file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"}, + {file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"}, + {file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"}, + {file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"}, + {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"}, + {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"}, + {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"}, + {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"}, + {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"}, + {file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"}, + {file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"}, + {file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"}, + {file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"}, + {file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"}, + {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"}, + {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"}, + {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"}, + {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"}, + {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"}, + {file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"}, + {file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"}, + {file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"}, + {file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"}, ] [package.dependencies] @@ -409,7 +394,6 @@ toml = ["tomli"] name = "cryptography" version = "39.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -455,7 +439,6 @@ tox = ["tox"] name = "cryptography" version = "40.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -497,7 +480,6 @@ tox = ["tox"] name = "cssselect" version = "1.2.0" description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -509,7 +491,6 @@ files = [ name = "distlib" version = "0.3.6" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -521,7 +502,6 @@ files = [ name = "dnspython" version = "2.3.0" description = "DNS toolkit" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -542,7 +522,6 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] name = "docutils" version = "0.18.1" description = "Docutils -- Python Documentation Utilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -554,7 +533,6 @@ files = [ name = "email-validator" version = "2.0.0.post2" description = "A robust email address syntax and deliverability validation library." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -570,7 +548,6 @@ idna = ">=2.0.0" name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -585,7 +562,6 @@ test = ["pytest (>=6)"] name = "faker" version = "18.9.0" description = "Faker is a Python package that generates fake data for you." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -601,7 +577,6 @@ typing-extensions = {version = ">=3.10.0.1", markers = "python_version < \"3.8\" name = "fancycompleter" version = "0.9.1" description = "colorful TAB completion for Python prompt" -category = "dev" optional = false python-versions = "*" files = [ @@ -617,7 +592,6 @@ pyrepl = ">=0.8.2" name = "filelock" version = "3.12.0" description = "A platform independent file lock." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -633,7 +607,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p name = "flask" version = "2.2.5" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -656,7 +629,6 @@ dotenv = ["python-dotenv"] name = "flask-babel" version = "3.1.0" description = "Adds i18n/l10n support for Flask applications." -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -674,7 +646,6 @@ pytz = ">=2022.7" name = "flask-themer" version = "1.4.3" description = "Simple theme mechanism for Flask" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -689,7 +660,6 @@ flask = "*" name = "flask-webtest" version = "0.1.3" description = "Utilities for testing Flask applications with WebTest." -category = "dev" optional = false python-versions = "*" files = [ @@ -709,7 +679,6 @@ tests = ["flask-sqlalchemy"] name = "flask-wtf" version = "1.1.1" description = "Form rendering, validation, and CSRF protection for Flask with WTForms." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -729,7 +698,6 @@ email = ["email-validator"] name = "freezegun" version = "1.2.2" description = "Let your Python tests travel through time" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -744,7 +712,6 @@ python-dateutil = ">=2.7" name = "honcho" version = "1.1.0" description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -762,7 +729,6 @@ export = ["jinja2 (>=2.7,<3)"] name = "identify" version = "2.5.24" description = "File identification library for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -777,7 +743,6 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -789,7 +754,6 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -801,7 +765,6 @@ files = [ name = "importlib-metadata" version = "6.6.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -822,7 +785,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -834,7 +796,6 @@ files = [ name = "itsdangerous" version = "2.1.2" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -846,7 +807,6 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -864,7 +824,6 @@ i18n = ["Babel (>=2.7)"] name = "lxml" version = "4.9.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -957,7 +916,6 @@ source = ["Cython (>=0.29.7)"] name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1017,7 +975,6 @@ files = [ name = "mock" version = "5.0.2" description = "Rolling backport of unittest.mock for all Pythons" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1034,7 +991,6 @@ test = ["pytest", "pytest-cov"] name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1049,7 +1005,6 @@ setuptools = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1061,7 +1016,6 @@ files = [ name = "pdbpp" version = "0.10.3" description = "pdb++, a drop-in replacement for pdb" -category = "dev" optional = false python-versions = "*" files = [ @@ -1082,7 +1036,6 @@ testing = ["funcsigs", "pytest"] name = "platformdirs" version = "3.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1101,7 +1054,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1120,7 +1072,6 @@ testing = ["pytest", "pytest-benchmark"] name = "portpicker" version = "1.5.2" description = "A library to choose unique available network ports." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1135,7 +1086,6 @@ psutil = "*" name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1155,7 +1105,6 @@ virtualenv = ">=20.10.0" name = "psutil" version = "5.9.5" description = "Cross-platform lib for process and system monitoring in Python." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1182,7 +1131,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1194,7 +1142,6 @@ files = [ name = "pyasn1-modules" version = "0.3.0" description = "A collection of ASN.1-based protocols modules" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1209,7 +1156,6 @@ pyasn1 = ">=0.4.6,<0.6.0" name = "pycountry" version = "22.3.5" description = "ISO country, subdivision, language, currency and script definitions and their translations" -category = "main" optional = false python-versions = ">=3.6, <4" files = [ @@ -1223,7 +1169,6 @@ setuptools = "*" name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1235,7 +1180,6 @@ files = [ name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1250,7 +1194,6 @@ plugins = ["importlib-metadata"] name = "pyquery" version = "2.0.0" description = "A jquery-like library for python" -category = "dev" optional = false python-versions = "*" files = [ @@ -1269,7 +1212,6 @@ test = ["pytest", "pytest-cov", "requests", "webob", "webtest"] name = "pyreadline" version = "2.1" description = "A python implmementation of GNU readline." -category = "dev" optional = false python-versions = "*" files = [ @@ -1280,7 +1222,6 @@ files = [ name = "pyrepl" version = "0.9.0" description = "A library for building flexible command line interfaces" -category = "dev" optional = false python-versions = "*" files = [ @@ -1291,7 +1232,6 @@ files = [ name = "pytest" version = "7.3.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1313,14 +1253,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-cov" -version = "4.0.0" +version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] @@ -1334,7 +1273,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-cover" version = "3.0.0" description = "Pytest plugin for measuring coverage. Forked from `pytest-cov`." -category = "dev" optional = false python-versions = "*" files = [ @@ -1349,7 +1287,6 @@ pytest-cov = ">=2.0" name = "pytest-coverage" version = "0.0" description = "Pytest plugin for measuring coverage. Forked from `pytest-cov`." -category = "dev" optional = false python-versions = "*" files = [ @@ -1364,7 +1301,6 @@ pytest-cover = "*" name = "pytest-flask" version = "1.2.0" description = "A set of py.test fixtures to test Flask applications." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1384,7 +1320,6 @@ docs = ["Sphinx", "sphinx-rtd-theme"] name = "pytest-httpserver" version = "1.0.6" description = "pytest-httpserver is a httpserver for pytest" -category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -1399,7 +1334,6 @@ Werkzeug = ">=2.0.0" name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1414,7 +1348,6 @@ six = ">=1.5" name = "python-ldap" version = "3.4.3" description = "Python modules for implementing LDAP clients" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1429,7 +1362,6 @@ pyasn1_modules = ">=0.1.5" name = "pytz" version = "2023.3" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -1441,7 +1373,6 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1489,14 +1420,13 @@ files = [ [[package]] name = "requests" -version = "2.30.0" +version = "2.31.0" description = "Python HTTP for Humans." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"}, - {file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] @@ -1513,7 +1443,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "sentry-sdk" version = "1.21.1" description = "Python client for Sentry (https://sentry.io)" -category = "main" optional = true python-versions = "*" files = [ @@ -1557,7 +1486,6 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "67.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1574,7 +1502,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1586,7 +1513,6 @@ files = [ name = "slapd" version = "0.1.3" description = "Controls a slapd process in a pythonic way" -category = "dev" optional = false python-versions = ">=3.7,<4" files = [ @@ -1598,7 +1524,6 @@ files = [ name = "smtpdfix" version = "0.5.0" description = "A SMTP server for use as a pytest fixture that implements encryption and authentication for testing." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1623,7 +1548,6 @@ typing = ["mypy"] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" optional = false python-versions = "*" files = [ @@ -1635,7 +1559,6 @@ files = [ name = "soupsieve" version = "2.4.1" description = "A modern CSS selector implementation for Beautiful Soup." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1647,7 +1570,6 @@ files = [ name = "sphinx" version = "5.3.0" description = "Python documentation generator" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1683,7 +1605,6 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] name = "sphinx-issues" version = "3.0.1" description = "A Sphinx extension for linking to your project's issue tracker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1701,14 +1622,13 @@ tests = ["pytest (>=6.2.0)"] [[package]] name = "sphinx-rtd-theme" -version = "1.2.0" +version = "1.2.1" description = "Read the Docs theme for Sphinx" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"}, - {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"}, + {file = "sphinx_rtd_theme-1.2.1-py2.py3-none-any.whl", hash = "sha256:2cc9351176cbf91944ce44cefd4fab6c3b76ac53aa9e15d6db45a3229ad7f866"}, + {file = "sphinx_rtd_theme-1.2.1.tar.gz", hash = "sha256:cf9a7dc0352cf179c538891cb28d6fad6391117d4e21c891776ab41dd6c8ff70"}, ] [package.dependencies] @@ -1723,7 +1643,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1739,7 +1658,6 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1755,7 +1673,6 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1771,7 +1688,6 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" -category = "dev" optional = false python-versions = ">=2.7" files = [ @@ -1786,7 +1702,6 @@ Sphinx = ">=1.8" name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1801,7 +1716,6 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1817,7 +1731,6 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1833,7 +1746,6 @@ test = ["pytest"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1845,7 +1757,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1855,21 +1766,19 @@ files = [ [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.6.2" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.6.2-py3-none-any.whl", hash = "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"}, + {file = "typing_extensions-4.6.2.tar.gz", hash = "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c"}, ] [[package]] name = "urllib3" version = "2.0.2" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1887,7 +1796,6 @@ zstd = ["zstandard (>=0.18.0)"] name = "virtualenv" version = "20.23.0" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1909,7 +1817,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess name = "waitress" version = "2.1.2" description = "Waitress WSGI server" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -1925,7 +1832,6 @@ testing = ["coverage (>=5.0)", "pytest", "pytest-cover"] name = "webob" version = "1.8.7" description = "WSGI request and response object" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" files = [ @@ -1941,7 +1847,6 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"] name = "webtest" version = "3.0.0" description = "Helper to test WSGI applications" -category = "dev" optional = false python-versions = ">=3.6, <4" files = [ @@ -1962,7 +1867,6 @@ tests = ["PasteDeploy", "WSGIProxy2", "coverage", "pyquery", "pytest", "pytest-c name = "werkzeug" version = "2.2.3" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1980,7 +1884,6 @@ watchdog = ["watchdog"] name = "wmctrl" version = "0.4" description = "A tool to programmatically control windows inside X" -category = "dev" optional = false python-versions = "*" files = [ @@ -1991,7 +1894,6 @@ files = [ name = "wtforms" version = "3.0.1" description = "Form validation and rendering for Python web development." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2009,7 +1911,6 @@ email = ["email-validator"] name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2027,4 +1928,4 @@ sentry = ["sentry-sdk"] [metadata] lock-version = "2.0" python-versions = ">=3.7, <4" -content-hash = "ba916dfc7340b0327fba0582d2153de589525f77820e411afc10b2fd19060c22" +content-hash = "ea6edd79777591d8c6232b5cd2d712a4de68e72475aca7b7c6417328eac5ea61" diff --git a/pyproject.toml b/pyproject.toml index ffab72cd..865d48f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ flask-themer = "<2" flask-wtf = "<2" pycountry = "^22.3.5" python-ldap = "<4" +pytz = "^2023.3" toml = "<1" wtforms = "<4" diff --git a/tests/app/test_forms.py b/tests/app/test_forms.py new file mode 100644 index 00000000..23877e8b --- /dev/null +++ b/tests/app/test_forms.py @@ -0,0 +1,186 @@ +import datetime + +import wtforms +from babel.dates import LOCALTZ +from canaille.app.forms import DateTimeUTCField +from flask import current_app +from werkzeug.datastructures import ImmutableMultiDict + + +def test_datetime_utc_field_no_timezone_is_local_timezone(testclient): + del current_app.config["TIMEZONE"] + + offset = LOCALTZ.utcoffset(datetime.datetime.utcnow()) + + class TestForm(wtforms.Form): + dt = DateTimeUTCField() + + form = TestForm() + form.validate() + assert form.dt.data is None + + utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc) + locale_date = datetime.datetime(2023, 6, 1, 12) + offset + rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S") + rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S") + + request_form = ImmutableMultiDict({"dt": rendered_locale_date_form}) + form = TestForm(request_form) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + form = TestForm(data={"dt": utc_date}) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + class Foobar: + dt = utc_date + + form = TestForm(obj=Foobar()) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + +def test_datetime_utc_field_utc(testclient): + current_app.config["TIMEZONE"] = "UTC" + + class TestForm(wtforms.Form): + dt = DateTimeUTCField() + + form = TestForm() + form.validate() + assert form.dt.data is None + + date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc) + rendered_date = date.strftime("%Y-%m-%d %H:%M:%S") + rendered_date_form = date.strftime("%Y-%m-%d %H:%M:%S") + + request_form = ImmutableMultiDict({"dt": rendered_date_form}) + form = TestForm(request_form) + assert form.validate() + assert form.dt.data == date + assert ( + form.dt() + == f'' + ) + + form = TestForm(data={"dt": date}) + assert form.validate() + assert form.dt.data == date + assert ( + form.dt() + == f'' + ) + + class Foobar: + dt = date + + form = TestForm(obj=Foobar()) + assert form.validate() + assert form.dt.data == date + assert ( + form.dt() + == f'' + ) + + +def test_datetime_utc_field_japan_timezone(testclient): + current_app.config["TIMEZONE"] = "Japan" + + class TestForm(wtforms.Form): + dt = DateTimeUTCField() + + form = TestForm() + form.validate() + assert form.dt.data is None + + utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc) + locale_date = datetime.datetime(2023, 6, 1, 21) + rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S") + rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S") + + request_form = ImmutableMultiDict({"dt": rendered_locale_date_form}) + form = TestForm(request_form) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + form = TestForm(data={"dt": utc_date}) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + class Foobar: + dt = utc_date + + form = TestForm(obj=Foobar()) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + +def test_datetime_utc_field_invalid_timezone(testclient): + current_app.config["TIMEZONE"] = "invalid" + + offset = LOCALTZ.utcoffset(datetime.datetime.utcnow()) + + class TestForm(wtforms.Form): + dt = DateTimeUTCField() + + form = TestForm() + form.validate() + assert form.dt.data is None + + utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc) + locale_date = datetime.datetime(2023, 6, 1, 12) + offset + rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S") + rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S") + + request_form = ImmutableMultiDict({"dt": rendered_locale_date_form}) + form = TestForm(request_form) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + form = TestForm(data={"dt": utc_date}) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) + + class Foobar: + dt = utc_date + + form = TestForm(obj=Foobar()) + assert form.validate() + assert form.dt.data == utc_date + assert ( + form.dt() + == f'' + ) diff --git a/tests/conftest.py b/tests/conftest.py index cfe2808d..dc9dfdd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,6 +76,7 @@ def configuration(slapd_server, smtpd): conf = { "SECRET_KEY": gen_salt(24), "LOGO": "/static/img/canaille-head.png", + "TIMEZONE": "UTC", "BACKENDS": { "LDAP": { "ROOT_DN": slapd_server.suffix,