diff --git a/poetry.lock b/poetry.lock index 5846474..cbf1756 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,6 +6,7 @@ version = "3.7.2" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, @@ -23,6 +24,7 @@ version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, @@ -42,6 +44,7 @@ version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, @@ -61,6 +64,7 @@ version = "2.0.4" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "autopep8-2.0.4-py2.py3-none-any.whl", hash = "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb"}, {file = "autopep8-2.0.4.tar.gz", hash = "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c"}, @@ -76,6 +80,7 @@ version = "2.2.1" description = "Function decoration for backoff and retry" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, @@ -87,6 +92,7 @@ version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, @@ -133,6 +139,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -144,6 +151,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -158,6 +166,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -169,6 +179,7 @@ version = "0.3.7" description = "serialize all of Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, @@ -183,6 +194,7 @@ version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, @@ -194,6 +206,7 @@ version = "0.5.0" description = "Use Database URLs in your Django Application." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, @@ -205,6 +218,7 @@ version = "4.2.19" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "Django-4.2.19-py3-none-any.whl", hash = "sha256:a104e13f219fc55996a4e416ef7d18ab4eeb44e0aa95174c192f16cda9f94e75"}, {file = "Django-4.2.19.tar.gz", hash = "sha256:6c833be4b0ca614f0a919472a1028a3bbdeb6f056fa04023aeb923346ba2c306"}, @@ -225,6 +239,7 @@ version = "3.18.2" description = "Run checks on services like databases, queue servers, celery processes, etc." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "django_health_check-3.18.2-py2.py3-none-any.whl", hash = "sha256:16f9c9186236cbc2858fa0d0ecc3566ba2ad2b72683e5678d0d58eb9e8bbba1a"}, {file = "django_health_check-3.18.2.tar.gz", hash = "sha256:21235120f8d756fa75ba430d0b0dbb04620fbd7bfac92ed6a0b911915ba38918"}, @@ -243,6 +258,7 @@ version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -257,6 +273,7 @@ version = "1.21.7" description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "drf-yasg-1.21.7.tar.gz", hash = "sha256:4c3b93068b3dfca6969ab111155e4dd6f7b2d680b98778de8fd460b7837bdb0d"}, {file = "drf_yasg-1.21.7-py3-none-any.whl", hash = "sha256:f85642072c35e684356475781b7ecf5d218fff2c6185c040664dd49f0a4be181"}, @@ -281,6 +298,7 @@ version = "9.2.0" description = "simplified environment variable parsing" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "environs-9.2.0-py2.py3-none-any.whl", hash = "sha256:10dca340bff9c912e99d237905909390365e32723c2785a9f3afa6ef426c53bc"}, {file = "environs-9.2.0.tar.gz", hash = "sha256:36081033ab34a725c2414f48ee7ec7f7c57e498d8c9255d61fbc7f2d4bf60865"}, @@ -302,6 +320,8 @@ version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, @@ -316,6 +336,7 @@ version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, @@ -332,6 +353,7 @@ version = "7.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, @@ -348,6 +370,7 @@ version = "1.4.0" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, @@ -356,12 +379,35 @@ files = [ [package.dependencies] python-dateutil = ">=2.7" +[[package]] +name = "gunicorn" +version = "23.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + [[package]] name = "identify" version = "2.5.33" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, @@ -376,6 +422,7 @@ version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, @@ -387,6 +434,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -398,6 +446,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -412,6 +461,7 @@ version = "1.10.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, @@ -458,6 +508,7 @@ version = "3.20.2" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, @@ -478,6 +529,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -489,6 +541,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -500,6 +553,7 @@ version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +groups = ["dev"] files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, @@ -514,6 +568,7 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, @@ -525,6 +580,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -536,6 +592,7 @@ version = "1.7.1" description = "Python style guide checker" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pep8-1.7.1-py2.py3-none-any.whl", hash = "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee"}, {file = "pep8-1.7.1.tar.gz", hash = "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374"}, @@ -547,6 +604,7 @@ version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, @@ -562,6 +620,7 @@ version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, @@ -577,6 +636,7 @@ version = "3.0.4" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pre_commit-3.0.4-py2.py3-none-any.whl", hash = "sha256:9e3255edb0c9e7fe9b4f328cb3dc86069f8fdc38026f1bf521018a05eaf4d67b"}, {file = "pre_commit-3.0.4.tar.gz", hash = "sha256:bc4687478d55578c4ac37272fe96df66f73d9b5cf81be6f28627d4e712e752d5"}, @@ -595,6 +655,7 @@ version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, @@ -676,6 +737,7 @@ version = "2.12.0" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, @@ -687,6 +749,7 @@ version = "3.2.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, @@ -698,6 +761,7 @@ version = "2.16.4" description = "python code static checker" optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "pylint-2.16.4-py3-none-any.whl", hash = "sha256:4a770bb74fde0550fa0ab4248a2ad04e7887462f9f425baa0cd8d3c1d098eaee"}, {file = "pylint-2.16.4.tar.gz", hash = "sha256:8841f26a0dbc3503631b6a20ee368b3f5e0e5461a1d95cf15d103dab748a0db3"}, @@ -726,6 +790,7 @@ version = "7.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, @@ -749,6 +814,7 @@ version = "4.8.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, @@ -767,6 +833,7 @@ version = "0.4.2" description = "Wrap tests with fixtures in freeze_time" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"}, {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"}, @@ -782,6 +849,7 @@ version = "3.10.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, @@ -799,6 +867,7 @@ version = "2.8.2" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -813,6 +882,7 @@ version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, @@ -827,6 +897,7 @@ version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, @@ -838,6 +909,7 @@ version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, @@ -857,7 +929,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -898,6 +969,7 @@ version = "70.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, @@ -913,6 +985,7 @@ version = "3.19.2" description = "Simple, fast, extensible JSON encoder/decoder for Python" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "simplejson-3.19.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3471e95110dcaf901db16063b2e40fb394f8a9e99b3fe9ee3acc6f6ef72183a2"}, {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3194cd0d2c959062b94094c0a9f8780ffd38417a5322450a0db0ca1a23e7fbd2"}, @@ -1020,6 +1093,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1031,6 +1105,7 @@ version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, @@ -1046,6 +1121,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1057,6 +1134,7 @@ version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, @@ -1068,6 +1146,8 @@ version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, @@ -1079,6 +1159,8 @@ version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, @@ -1090,6 +1172,7 @@ version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, @@ -1101,6 +1184,7 @@ version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, @@ -1121,6 +1205,7 @@ version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, @@ -1195,6 +1280,6 @@ files = [ ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "1515eabf51c69c1fdccd97222bbfad292a0b7b8ac0b95a6857ac482b22ec4b2c" +content-hash = "8eb67f28bbfd77949f2dccae5d2863fdd3ef00d5377fb0d9059b22b49fc1cc03" diff --git a/pyproject.toml b/pyproject.toml index 23f9808..89464fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,14 +24,18 @@ exclude = ''' ''' [tool.isort] -use_parentheses=true -multi_line_output=3 -include_trailing_comma=true -line_length=79 -known_first_party=['task_processor'] -known_third_party=['django', 'rest_framework', 'saml2', 'drf_yasg2', 'pytest'] +use_parentheses = true +multi_line_output = 3 +include_trailing_comma = true +line_length = 79 +known_first_party = ['task_processor'] +known_third_party = ['django', 'rest_framework', 'saml2', 'drf_yasg2', 'pytest'] skip = ['migrations', 'flagsmith', '.venv'] +[tool.pytest.ini_options] +addopts = ['--ds=tests.settings', '-vvvv', '-p', 'no:warnings'] +console_output_style = 'count' + [tool.poetry] name = "flagsmith_task_processor" version = "1.0.0" @@ -39,7 +43,7 @@ description = "Task Processor plugin for Flagsmith application." authors = ["Flagsmith "] readme = "readme.md" include = [{ path = "migrations/sql/*", format = ["sdist", "wheel"] }] -packages = [{ include = "task_processor"}] +packages = [{ include = "task_processor" }] [tool.poetry.dependencies] python = ">=3.10,<4.0" @@ -50,6 +54,7 @@ drf-yasg = "~1.21.6" dj-database-url = "~0.5.0" environs = "~9.2.0" psycopg2-binary = "~2.9.5" +gunicorn = "*" [tool.poetry.group.dev.dependencies] django = "~4.2.18" diff --git a/task_processor/management/commands/checktaskprocessorthreadhealth.py b/task_processor/management/commands/checktaskprocessorthreadhealth.py deleted file mode 100644 index f3eda61..0000000 --- a/task_processor/management/commands/checktaskprocessorthreadhealth.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging -import sys - -from django.core.management import BaseCommand - -from task_processor.thread_monitoring import get_unhealthy_thread_names - -logger = logging.getLogger(__name__) - - -class Command(BaseCommand): - def handle(self, *args, **options): - if get_unhealthy_thread_names(): - sys.exit("Task processor has unhealthy threads.") - - logger.info("Task processor has no unhealthy threads.") - sys.exit(0) diff --git a/task_processor/management/commands/runprocessor.py b/task_processor/management/commands/runprocessor.py index 15dc11c..fed66d5 100644 --- a/task_processor/management/commands/runprocessor.py +++ b/task_processor/management/commands/runprocessor.py @@ -1,18 +1,12 @@ import logging -import signal -import time from argparse import ArgumentParser -from datetime import timedelta from django.core.management import BaseCommand -from django.utils import timezone +from gunicorn.config import Config -from task_processor.task_registry import initialise -from task_processor.thread_monitoring import ( - clear_unhealthy_threads, - write_unhealthy_threads, -) -from task_processor.threads import TaskRunner +from task_processor.threads import TaskRunner, TaskRunnerCoordinator +from task_processor.types import TaskProcessorConfig +from task_processor.utils import run_server logger = logging.getLogger(__name__) @@ -21,9 +15,6 @@ class Command(BaseCommand): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - signal.signal(signal.SIGINT, self._exit_gracefully) - signal.signal(signal.SIGTERM, self._exit_gracefully) - self._threads: list[TaskRunner] = [] self._monitor_threads = True @@ -52,60 +43,27 @@ def add_arguments(self, parser: ArgumentParser): help="Number of tasks each worker will pop from the queue on each cycle.", default=10, ) - - def handle(self, *args, **options): - num_threads = options["numthreads"] - sleep_interval_ms = options["sleepintervalms"] - grace_period_ms = options["graceperiodms"] - queue_pop_size = options["queuepopsize"] - - logger.debug( - "Running task processor with args: %s", - ",".join([f"{k}={v}" for k, v in options.items()]), + parser.add_subparsers(dest="gunicorn").add_parser( + "gunicorn arguments", + add_help=False, + aliases=["gunicorn"], + parents=[Config().parser()], ) - self._threads.extend( - [ - TaskRunner( - sleep_interval_millis=sleep_interval_ms, - queue_pop_size=queue_pop_size, - ) - for _ in range(num_threads) - ] + def handle(self, *args, **options): + config = TaskProcessorConfig( + num_threads=options["numthreads"], + sleep_interval_ms=options["sleepintervalms"], + grace_period_ms=options["graceperiodms"], + queue_pop_size=options["queuepopsize"], ) - logger.info("Processor starting") - - initialise() - - for thread in self._threads: - thread.start() - - clear_unhealthy_threads() - while self._monitor_threads: - time.sleep(1) - unhealthy_threads = self._get_unhealthy_threads( - ms_before_unhealthy=grace_period_ms + sleep_interval_ms - ) - if unhealthy_threads: - write_unhealthy_threads(unhealthy_threads) - - [t.join() for t in self._threads] - - def _exit_gracefully(self, *args): - self._monitor_threads = False - for t in self._threads: - t.stop() + logger.debug("Config: %s", config) - def _get_unhealthy_threads(self, ms_before_unhealthy: int) -> list[TaskRunner]: - unhealthy_threads = [] - healthy_threshold = timezone.now() - timedelta(milliseconds=ms_before_unhealthy) + coordinator = TaskRunnerCoordinator(config=config) + coordinator.start() - for thread in self._threads: - if ( - not thread.is_alive() - or not thread.last_checked_for_tasks - or thread.last_checked_for_tasks < healthy_threshold - ): - unhealthy_threads.append(thread) - return unhealthy_threads + try: + run_server(options=options) + finally: + coordinator.stop() diff --git a/task_processor/thread_monitoring.py b/task_processor/thread_monitoring.py deleted file mode 100644 index d8c8078..0000000 --- a/task_processor/thread_monitoring.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import logging -import os -import typing -from threading import Thread - -UNHEALTHY_THREADS_FILE_PATH = "/tmp/task-processor-unhealthy-threads.json" - -logger = logging.getLogger(__name__) - - -def clear_unhealthy_threads(): - if _unhealthy_threads_file_exists(): - os.remove(UNHEALTHY_THREADS_FILE_PATH) - - -def write_unhealthy_threads(unhealthy_threads: typing.List[Thread]): - unhealthy_thread_names = [t.name for t in unhealthy_threads] - logger.warning("Writing unhealthy threads: %s", unhealthy_thread_names) - - with open(UNHEALTHY_THREADS_FILE_PATH, "w+") as f: - f.write(json.dumps(unhealthy_thread_names)) - - -def get_unhealthy_thread_names() -> typing.List[str]: - if not _unhealthy_threads_file_exists(): - return [] - - with open(UNHEALTHY_THREADS_FILE_PATH, "r") as f: - return json.loads(f.read()) - - -def _unhealthy_threads_file_exists(): - return os.path.exists(UNHEALTHY_THREADS_FILE_PATH) diff --git a/task_processor/threads.py b/task_processor/threads.py index 42973fb..aec3c1e 100644 --- a/task_processor/threads.py +++ b/task_processor/threads.py @@ -1,15 +1,76 @@ import logging import time +from datetime import timedelta from threading import Thread from django.db import close_old_connections from django.utils import timezone from task_processor.processor import run_recurring_tasks, run_tasks +from task_processor.task_registry import initialise +from task_processor.types import TaskProcessorConfig logger = logging.getLogger(__name__) +class TaskRunnerCoordinator(Thread): + def __init__( + self, + *args, + config: TaskProcessorConfig, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.config = config + self._threads: list[TaskRunner] = [] + self._monitor_threads = True + + def run(self) -> None: + initialise() + + logger.info("Processor starting") + + for _ in range(self.config.num_threads): + self._threads.append( + task := TaskRunner( + sleep_interval_millis=self.config.sleep_interval_ms, + queue_pop_size=self.config.queue_pop_size, + ) + ) + task.start() + + ms_before_unhealthy = ( + self.config.grace_period_ms + self.config.sleep_interval_ms + ) + while self._monitor_threads: + time.sleep(1) + unhealthy_threads = self._get_unhealthy_threads( + ms_before_unhealthy=ms_before_unhealthy + ) + if unhealthy_threads: + logger.warning("%d unhealthy threads detected", len(unhealthy_threads)) + + [t.join() for t in self._threads] + + def _get_unhealthy_threads(self, ms_before_unhealthy: int) -> list["TaskRunner"]: + unhealthy_threads = [] + healthy_threshold = timezone.now() - timedelta(milliseconds=ms_before_unhealthy) + + for thread in self._threads: + if ( + not thread.is_alive() + or not thread.last_checked_for_tasks + or thread.last_checked_for_tasks < healthy_threshold + ): + unhealthy_threads.append(thread) + return unhealthy_threads + + def stop(self) -> None: + self._monitor_threads = False + for t in self._threads: + t.stop() + + class TaskRunner(Thread): def __init__( self, diff --git a/task_processor/types.py b/task_processor/types.py new file mode 100644 index 0000000..2c23649 --- /dev/null +++ b/task_processor/types.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class TaskProcessorConfig: + num_threads: int + sleep_interval_ms: int + grace_period_ms: int + queue_pop_size: int diff --git a/task_processor/utils.py b/task_processor/utils.py new file mode 100644 index 0000000..6df1424 --- /dev/null +++ b/task_processor/utils.py @@ -0,0 +1,33 @@ +from gunicorn.app.wsgiapp import WSGIApplication as GunicornWSGIApplication + + +class _WSGIApplication(GunicornWSGIApplication): + def __init__( + self, + app_uri: str, + options: dict[str, str] | None = None, + ) -> None: + self.options = options or {} + self.app_uri = app_uri + super().__init__() + + def load_config(self) -> None: + config = { + key: value + for key, value in self.options.items() + if key in self.cfg.settings and value is not None + } + for key, value in config.items(): + self.cfg.set(key.lower(), value) + + +def run_server( + app_uri: str = "app.wsgi", + options: dict[str, str] | None = None, +) -> None: + options = options or {} + # Defaults suitable for Task processor configuration + # intended to only serve the health check endpoints + options.setdefault("worker_class", "sync") + options.setdefault("workers", 1) + _WSGIApplication(app_uri, options).run() diff --git a/tests/unit/task_processor/test_unit_task_processor_thread_monitoring.py b/tests/unit/task_processor/test_unit_task_processor_thread_monitoring.py deleted file mode 100644 index 2be1326..0000000 --- a/tests/unit/task_processor/test_unit_task_processor_thread_monitoring.py +++ /dev/null @@ -1,89 +0,0 @@ -import json -import logging -from threading import Thread -from unittest.mock import mock_open, patch - -from task_processor.thread_monitoring import ( - UNHEALTHY_THREADS_FILE_PATH, - clear_unhealthy_threads, - get_unhealthy_thread_names, - write_unhealthy_threads, -) - - -def test_clear_unhealthy_threads(mocker): - # Given - mocked_os = mocker.patch("task_processor.thread_monitoring.os") - - def os_path_side_effect(file_path): - return file_path == UNHEALTHY_THREADS_FILE_PATH - - mocked_os.path.exists.side_effect = os_path_side_effect - - # When - clear_unhealthy_threads() - - # Then - mocked_os.remove.assert_called_once_with(UNHEALTHY_THREADS_FILE_PATH) - - -def test_write_unhealthy_threads(caplog, settings): - # Given - # caplog doesn't allow you to capture logging outputs from loggers that don't - # propagate to root. Quick hack here to get the task_processor logger to - # propagate. - # TODO: look into using loguru. - task_processor_logger = logging.getLogger("task_processor") - task_processor_logger.propagate = True - - threads = [Thread(target=lambda: None)] - - # When - with patch("builtins.open", mock_open()) as mocked_open: - write_unhealthy_threads(threads) - - # Then - mocked_open.assert_called_once_with(UNHEALTHY_THREADS_FILE_PATH, "w+") - mocked_open.return_value.write.assert_called_once_with( - json.dumps([t.name for t in threads]) - ) - assert len(caplog.records) == 1 - assert caplog.record_tuples[0][1] == 30 # WARNING - assert caplog.record_tuples[0][2] == "Writing unhealthy threads: %s" % [ - t.name for t in threads - ] - - -def test_get_unhealthy_thread_names_returns_empty_list_if_file_does_not_exist(mocker): - # Given - mocked_os = mocker.patch("task_processor.thread_monitoring.os") - mocked_os.path.exists.return_value = False - - # When - unhealthy_thread_names = get_unhealthy_thread_names() - - # Then - assert unhealthy_thread_names == [] - - -def test_get_unhealthy_thread_names(mocker): - # Given - mocked_os = mocker.patch("task_processor.thread_monitoring.os") - - def os_path_side_effect(file_path): - return file_path == UNHEALTHY_THREADS_FILE_PATH - - mocked_os.path.exists.side_effect = os_path_side_effect - - expected_unhealthy_thread_names = ["Thread-1", "Thread-2"] - - # When - with patch( - "builtins.open", - mock_open(read_data=json.dumps(expected_unhealthy_thread_names)), - ) as mocked_open: - unhealthy_thread_names = get_unhealthy_thread_names() - - # Then - mocked_open.assert_called_once_with(UNHEALTHY_THREADS_FILE_PATH, "r") - assert unhealthy_thread_names == expected_unhealthy_thread_names