├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── .pylintrc ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── assets ├── Group.png └── pytaskio.jpg ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ └── pytask_io.rst ├── pyproject.toml ├── pytask_io ├── __init__.py ├── actions.py ├── client.py ├── event_loop.py ├── exceptions.py ├── logger.py ├── pytask_io.py ├── store.py ├── task_queue.py ├── utils.py └── worker.py ├── setup.py └── tests ├── __init__.py ├── fixtures.py ├── mock_scenario_one.py ├── mock_uow.py ├── test_client.py ├── test_pytask_io.py ├── test_task_queue.py └── test_worker.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | .idea/ 127 | .idea 128 | 129 | # Visual Studio Code 130 | .vscode/ 131 | local.py -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=3 3 | include_trailing_comma=True 4 | force_grid_wrap=0 5 | use_parentheses=True 6 | line_length=88 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - repo: local 7 | hooks: 8 | - id: black 9 | language_version: python3.6 10 | name: black 11 | entry: black 12 | language: system 13 | types: [python] 14 | - repo: local 15 | hooks: 16 | - id: isort 17 | name: isort 18 | entry: isort 19 | language: system 20 | types: [python] 21 | - repo: local 22 | hooks: 23 | - id: pylint 24 | name: pylint 25 | entry: pylint 26 | language: system 27 | types: [python] 28 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | [MESSAGES CONTROL] 4 | disable=too-many-arguments, 5 | too-few-public-methods 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Changed 4 | 5 | **Release 0.0.4** - 2020-02-29 6 | 7 | - Updates the library install path to `PytaskIO` class & removes `requirements.txt` [Issue #32](https://github.com/joegasewicz/pytask-io/issues/32) 8 | - Adds `Pipefile` & `Pipfile.lock` / pipenv to the project [Issue #31](https://github.com/joegasewicz/pytask-io/pull/31) 9 | 10 | **Release 0.0.5** - 2020-02-29 11 | - Remove unused packages [Issue #35](https://github.com/joegasewicz/pytask-io/issues/35) 12 | - Improve `Pipfile` & `setup.py` to allow in-place editing using pipenv. 13 | 14 | ## Unreleased 15 | - Update pipenv to python 3.10 [Issue #74](https://github.com/joegasewicz/pytask-io/issues/74) 16 | - TestPyTaskIO::test_init unit test fails [Issue #39](https://github.com/joegasewicz/pytask-io/issues/39) 17 | - TestPyTaskIO::test_worker unit test fails [Issue #41](https://github.com/joegasewicz/pytask-io/issues/41) 18 | - Mixed licenses [Issue #57](https://github.com/joegasewicz/pytask-io/issues/57) 19 | - TestPyTaskIO::test_add_unit_of_work unit test fails [Issue #38](https://github.com/joegasewicz/pytask-io/issues/38) 20 | - Fix test_add_task [Issue #24](https://github.com/joegasewicz/pytask-io/issues/24) 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2021 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | redis: 2 | docker run -d -p 6379:6379 redis 3 | 4 | test: 5 | pipenv run pytest -vvv -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | Sphinx = "*" 8 | pampy = "*" 9 | importlib-metadata = "*" 10 | pylint = "*" 11 | black = "*" 12 | isort = "*" 13 | pre-commit = "*" 14 | freezegun = "*" 15 | pytest = "*" 16 | pytest-asyncio = "*" 17 | 18 | [packages] 19 | dill = "*" 20 | redis = "*" 21 | 22 | [pipenv] 23 | allow_prereleases = true 24 | 25 | [requires] 26 | python_version = "3.10" 27 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "439838df645e99ace086b12caf7472577072f6aad0b830649281b91fa7e3fe13" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "deprecated": { 20 | "hashes": [ 21 | "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", 22 | "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 25 | "version": "==1.2.14" 26 | }, 27 | "dill": { 28 | "hashes": [ 29 | "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f", 30 | "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675" 31 | ], 32 | "index": "pypi", 33 | "markers": "python_version >= '2.7' and python_version != '3.0'", 34 | "version": "==0.3.4" 35 | }, 36 | "redis": { 37 | "hashes": [ 38 | "sha256:c8481cf414474e3497ec7971a1ba9b998c8efad0f0d289a009a5bbef040894f9", 39 | "sha256:ccf692811f2c1fc7a92b466aa2599e4a6d2d73d5f736a2c70be600657c0da34a" 40 | ], 41 | "index": "pypi", 42 | "markers": "python_version >= '3.6'", 43 | "version": "==4.0.2" 44 | }, 45 | "wrapt": { 46 | "hashes": [ 47 | "sha256:01592f7b69b0e721146eed35f0e73f64d06c4caf449d630382863e1f1709150d", 48 | "sha256:09695a747c4af43a50425a45badebdb932d026d6e4d57b885e328f1294a14825", 49 | "sha256:115970769617c7d03d23ecfa08c174f29a9bbf8da08ded204702bc34b76658ab", 50 | "sha256:169ae1817e18a8ae3e10796b1e0c203e04d83ca487f64e7ddb22077eae9a1915", 51 | "sha256:16e5d269660b7217a7a53631df87900ee8937038fae6a3adb88411b108bb8f50", 52 | "sha256:17e978083c577ee724c8eeb0155ed96dd7a1e50b97f30c535da1c09cda0970c9", 53 | "sha256:1e3df5fd073559d32ad8cf6ee10d55e956ed794c130b24c57769416a13c37a60", 54 | "sha256:221eca686d29b2babd09007296816fe338e502a002c965b27737d108e2ce1832", 55 | "sha256:228533d82158a4d25de8b3703195d5c69bae5860010c75bd01f6ab393561ea22", 56 | "sha256:22c6ba8b141ec9b6e34beff8429bd03cd082987dd4831d288b2931fee6c34d29", 57 | "sha256:2522e09d2f8fc0f1db4aafdeebe84597c078cd7d70cc56b1bbc4887870b1f24c", 58 | "sha256:29c20ca9bab38759a1d8fb171ad17f7da5ec18855fc7aacd6c93edc83dbc9764", 59 | "sha256:29d70cbf5814bf91193cdac1ffc76240adfeb7c89a8070dc29e49674249542c2", 60 | "sha256:2e287347a241c3db125d58ec0c0c9f363fd7cbfdf2affc352c0f5bd4d0f0b799", 61 | "sha256:378021d27d168f6f09cf69eaaa9052c8ea48e103beb7d403fc978ad7d6dcdc3e", 62 | "sha256:399a64e99b3a327159e79cc20b6f0552e72e32bf6155d5521b0496cab26d1110", 63 | "sha256:3a7e40704f48038f5233b993d61ac3195beb748cfe8e8f4698349c5fecbe6280", 64 | "sha256:3b13c53d8629cced58bdee9ee3c3a4372097660dad7e8398b3f26267a942cb2e", 65 | "sha256:3ca9ab90a5d6fa91761a6151018819a9239b28c9cc2e8e1d679833bd0f5f939c", 66 | "sha256:3cf2fae5a9757491ed8756f580ece8312fea95e07be2b340ddef982a766a7ac7", 67 | "sha256:3ee64ab62c8fdc671c16b6d44bee12d598e0ad936b3a17e891404fd79f1f3d61", 68 | "sha256:4148753415a77f674a9f3ed372d68f9b6c0e83e6f2bcf91d3068462a40cb43cd", 69 | "sha256:4409bd44e5ffa1656bce5c8824155abc0e3151d07393c3434a28f812c5a123d0", 70 | "sha256:467ca88d704655f3e7429af2fa4649d602cb27ce289f26b833c207bf11d96830", 71 | "sha256:4c51ab4213d97f9e2287c5dd9f61fc44a2604be79246239ebadeae636b274596", 72 | "sha256:4e07ac0a4f44693bfc8df134b181268adfcdd7e73db9814901df3e051d9266fc", 73 | "sha256:4f31b53e611854ab300535f81038a83dbcd472814dd3a9970eeb201578df6262", 74 | "sha256:54be66645d5b2358b9294ff0349005789541ac9a4c3a10d60042685e2ea51ca0", 75 | "sha256:55d9cbee700697ae3a5a34045446d0890baada178fe6028604e8f2c9992470cb", 76 | "sha256:5816dee9ab69a18ca47a0d1d67086b2995910e11b7a0a2a2bae6e9ac63ac2828", 77 | "sha256:59ecece1b2b35d5fc6f1605d15e90d4342b0658b17158643e6a17b72e38da826", 78 | "sha256:5b3f2cef94b53f0d55d96c6309f1100110475c7b1fb907bc5f3fd2b0c940b238", 79 | "sha256:604dad6f6b34d767097b12a1ae84a128d899626c78e86e1180eff35d64f1a57d", 80 | "sha256:640fdfcb41865941c2fa4c0dfb9db6ab389d65e3266a464afeeff23f8f77fb24", 81 | "sha256:65586e7a33267e5725cf228c0f7b9e819ab60c0c88ccf9827c2e0526d43a1103", 82 | "sha256:6bb06bfe4c53f65d59bbe9101a8181c5e8dcde1ad0f778a4d502b599ebc213e6", 83 | "sha256:6d4950d0b5ebbe74ca549b2000474330988c88ae59a67b961e2cfdc18cd75003", 84 | "sha256:706bddba81c86330d92bbf080afef4a8c4f4fd86e0bc4a1975fac9b84c6758af", 85 | "sha256:76263e0c1207dfe9099805d1cf6147de05f638a598c8453cd8bb914aaf7520db", 86 | "sha256:77cd63017a8c35ead1a07f85c3ec4fa259bb2260332d69c6e9ae4c35b2c8e79f", 87 | "sha256:793c91e9c86d80850f2f40f1d3d5dce4f810f5aa6e5a80310fb8d32f5210f4df", 88 | "sha256:7d71ac38f1f178a8d3139e5560d95dabd4f894820a5627d0cd7535e9a255056a", 89 | "sha256:7e372d054af5b9652c7ba05fda93aec3bcb8f53dab21e61249c98a1596e48402", 90 | "sha256:8c9b972bc3dad5363f966912b8134b2ebfc1dc5a030e0835dc6c60f107390fe7", 91 | "sha256:969d518cc42be5f9f78fb7e6f42e51796e1ebce02219c93a03fcb29a7a3eb1e7", 92 | "sha256:a3461740b336424b836840f3568a56775e5fc988521cb6072aa3c3f2a589036c", 93 | "sha256:a3a15c874a1a30a9c4edb5ec55d96c1210f5974df51a6d69a367aace74378467", 94 | "sha256:a3fdde5f045de444875fa2c6822a1551dd03dcbb3a22fb52ded73744d7ebad55", 95 | "sha256:a7675ff09d87435b8f678d17e78211cb589ae805fd31579dc918e23c71710a6c", 96 | "sha256:a9a65f98a571083dc61419295aeb8d59170227227d4ba13d6d5b96a953a519aa", 97 | "sha256:b265aaab64dfb9db412ee315ff23f52f1986cf6e0989d719d90423baf4019d63", 98 | "sha256:b96bbcbf81ee9ee2e0c81a5f2a3bd0975a6dc0a6a9fc335f9b302a661999e3ed", 99 | "sha256:bdbb31db39b69d0f0e5ae83f99b8e28fa3ad7b7e05de6c5faa3cd52a4de20ef5", 100 | "sha256:c3f3c320272601223a036fecd942dd1258a15cebe88e18012cc88b2e6b813483", 101 | "sha256:c85b8fbd7c0e303a6d6e5731a7898f10e070dead30822b0481327dba74e7123c", 102 | "sha256:c939d2cc4015311e8aae68f52a6bc8e69c02b2c7166953fef9c1f06657aabdfc", 103 | "sha256:d6a866d5b8fc0f713440aafb9507f688f4d660c2f868093ae6cd0acb62d1a918", 104 | "sha256:d7c7e203e93f1eed57880f84505e7d2b4ece02e3ed7c6690ba90d0385f1a74b4", 105 | "sha256:d8d2fda2907c42ba3df720f0c7a704f36e09c586c8f7a4eed8b9db0e1d2fa79a", 106 | "sha256:db39f9065ed5b30081f8df71ced66cd29640b21e0091e2e5572ba0d70078f611", 107 | "sha256:db4772a9498023ce19f95b7aa86a8d94c8838269597361a986133373990be41f", 108 | "sha256:dd5dd67c074201664e4b80022128ef6eee8a007f2281d7099cd2114061af796a", 109 | "sha256:dfc2a91a23de91c9cedc9bc34742469ad7d1f177d4bf1a7a359af1adf9050e9b", 110 | "sha256:e5b450194731714cba9267e5c04e1f3622090c3ec43e1acba36744d92354da5e", 111 | "sha256:e7b324089cb59d700ed5cace6b39013f81473d1bf410da77a0d304ebadbc6eb9", 112 | "sha256:ea6041327840465f2450a1c8894a834a99aa4626a96f82547d9c042526eb1487", 113 | "sha256:eb72f906837cea3583fd9c91c6e286f8616360767703c837e7dfa1a70b123ad0", 114 | "sha256:f0b9edc564b1af9e9ac9cf932349136c74894ce2f699e00c1279b0fa5909d515", 115 | "sha256:f9e10c3bf07074377fbbff3d2b02d740c17602ce5d6c91455977bdb32fbbebe8", 116 | "sha256:fc43eb869c6baba54dda3264109354a5d0ea621c51ba945ff71308347f24a1c9" 117 | ], 118 | "markers": "python_version >= '3.6'", 119 | "version": "==1.16.0rc1" 120 | } 121 | }, 122 | "develop": { 123 | "alabaster": { 124 | "hashes": [ 125 | "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", 126 | "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2" 127 | ], 128 | "markers": "python_version >= '3.6'", 129 | "version": "==0.7.13" 130 | }, 131 | "astroid": { 132 | "hashes": [ 133 | "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877", 134 | "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6" 135 | ], 136 | "markers": "python_full_version >= '3.6.2'", 137 | "version": "==2.9.3" 138 | }, 139 | "attrs": { 140 | "hashes": [ 141 | "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", 142 | "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" 143 | ], 144 | "markers": "python_version >= '3.7'", 145 | "version": "==23.1.0" 146 | }, 147 | "babel": { 148 | "hashes": [ 149 | "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210", 150 | "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec" 151 | ], 152 | "markers": "python_version >= '3.7'", 153 | "version": "==2.13.0" 154 | }, 155 | "black": { 156 | "hashes": [ 157 | "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", 158 | "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", 159 | "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", 160 | "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", 161 | "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", 162 | "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", 163 | "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", 164 | "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", 165 | "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", 166 | "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", 167 | "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", 168 | "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", 169 | "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", 170 | "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", 171 | "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", 172 | "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", 173 | "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", 174 | "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", 175 | "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", 176 | "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", 177 | "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", 178 | "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" 179 | ], 180 | "index": "pypi", 181 | "markers": "python_version >= '3.8'", 182 | "version": "==24.3.0" 183 | }, 184 | "certifi": { 185 | "hashes": [ 186 | "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", 187 | "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" 188 | ], 189 | "index": "pypi", 190 | "markers": "python_version >= '3.6'", 191 | "version": "==2024.7.4" 192 | }, 193 | "cfgv": { 194 | "hashes": [ 195 | "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", 196 | "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" 197 | ], 198 | "markers": "python_version >= '3.8'", 199 | "version": "==3.4.0" 200 | }, 201 | "charset-normalizer": { 202 | "hashes": [ 203 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 204 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 205 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 206 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 207 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 208 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 209 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 210 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 211 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 212 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 213 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 214 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 215 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 216 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 217 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 218 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 219 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 220 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 221 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 222 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 223 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 224 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 225 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 226 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 227 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 228 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 229 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 230 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 231 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 232 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 233 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 234 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 235 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 236 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 237 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 238 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 239 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 240 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 241 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 242 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 243 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 244 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 245 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 246 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 247 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 248 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 249 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 250 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 251 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 252 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 253 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 254 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 255 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 256 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 257 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 258 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 259 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 260 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 261 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 262 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 263 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 264 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 265 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 266 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 267 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 268 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 269 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 270 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 271 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 272 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 273 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 274 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 275 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 276 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 277 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 278 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 279 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 280 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 281 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 282 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 283 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 284 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 285 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 286 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 287 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 288 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 289 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 290 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 291 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 292 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 293 | ], 294 | "markers": "python_full_version >= '3.7.0'", 295 | "version": "==3.3.2" 296 | }, 297 | "click": { 298 | "hashes": [ 299 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 300 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 301 | ], 302 | "markers": "python_version >= '3.7'", 303 | "version": "==8.1.7" 304 | }, 305 | "distlib": { 306 | "hashes": [ 307 | "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", 308 | "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" 309 | ], 310 | "version": "==0.3.9" 311 | }, 312 | "docutils": { 313 | "hashes": [ 314 | "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", 315 | "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" 316 | ], 317 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 318 | "version": "==0.17.1" 319 | }, 320 | "filelock": { 321 | "hashes": [ 322 | "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", 323 | "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" 324 | ], 325 | "markers": "python_version >= '3.8'", 326 | "version": "==3.16.1" 327 | }, 328 | "freezegun": { 329 | "hashes": [ 330 | "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3", 331 | "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712" 332 | ], 333 | "index": "pypi", 334 | "markers": "python_version >= '3.5'", 335 | "version": "==1.1.0" 336 | }, 337 | "identify": { 338 | "hashes": [ 339 | "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54", 340 | "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d" 341 | ], 342 | "markers": "python_version >= '3.8'", 343 | "version": "==2.5.30" 344 | }, 345 | "idna": { 346 | "hashes": [ 347 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 348 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 349 | ], 350 | "index": "pypi", 351 | "markers": "python_version >= '3.5'", 352 | "version": "==3.7" 353 | }, 354 | "imagesize": { 355 | "hashes": [ 356 | "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", 357 | "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" 358 | ], 359 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 360 | "version": "==1.4.1" 361 | }, 362 | "importlib-metadata": { 363 | "hashes": [ 364 | "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", 365 | "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" 366 | ], 367 | "index": "pypi", 368 | "markers": "python_version >= '3.6'", 369 | "version": "==4.8.2" 370 | }, 371 | "iniconfig": { 372 | "hashes": [ 373 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 374 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 375 | ], 376 | "markers": "python_version >= '3.7'", 377 | "version": "==2.0.0" 378 | }, 379 | "isort": { 380 | "hashes": [ 381 | "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", 382 | "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" 383 | ], 384 | "index": "pypi", 385 | "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", 386 | "version": "==5.10.1" 387 | }, 388 | "jinja2": { 389 | "hashes": [ 390 | "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", 391 | "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" 392 | ], 393 | "index": "pypi", 394 | "markers": "python_version >= '3.7'", 395 | "version": "==3.1.5" 396 | }, 397 | "lazy-object-proxy": { 398 | "hashes": [ 399 | "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", 400 | "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", 401 | "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", 402 | "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", 403 | "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", 404 | "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", 405 | "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", 406 | "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", 407 | "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", 408 | "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", 409 | "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", 410 | "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", 411 | "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", 412 | "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", 413 | "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", 414 | "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", 415 | "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", 416 | "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", 417 | "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", 418 | "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", 419 | "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", 420 | "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", 421 | "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", 422 | "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", 423 | "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", 424 | "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", 425 | "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", 426 | "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", 427 | "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", 428 | "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", 429 | "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", 430 | "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", 431 | "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", 432 | "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", 433 | "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", 434 | "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" 435 | ], 436 | "markers": "python_version >= '3.7'", 437 | "version": "==1.9.0" 438 | }, 439 | "markupsafe": { 440 | "hashes": [ 441 | "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", 442 | "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", 443 | "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", 444 | "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", 445 | "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", 446 | "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", 447 | "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", 448 | "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", 449 | "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", 450 | "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", 451 | "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", 452 | "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", 453 | "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", 454 | "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", 455 | "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", 456 | "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", 457 | "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", 458 | "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", 459 | "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", 460 | "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", 461 | "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", 462 | "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", 463 | "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", 464 | "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", 465 | "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", 466 | "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", 467 | "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", 468 | "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", 469 | "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", 470 | "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", 471 | "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", 472 | "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", 473 | "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", 474 | "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", 475 | "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", 476 | "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", 477 | "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", 478 | "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", 479 | "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", 480 | "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", 481 | "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", 482 | "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", 483 | "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", 484 | "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", 485 | "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", 486 | "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", 487 | "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", 488 | "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", 489 | "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", 490 | "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", 491 | "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", 492 | "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", 493 | "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", 494 | "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", 495 | "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", 496 | "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", 497 | "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", 498 | "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", 499 | "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", 500 | "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", 501 | "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" 502 | ], 503 | "markers": "python_version >= '3.9'", 504 | "version": "==3.0.2" 505 | }, 506 | "mccabe": { 507 | "hashes": [ 508 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 509 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 510 | ], 511 | "version": "==0.6.1" 512 | }, 513 | "mypy-extensions": { 514 | "hashes": [ 515 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 516 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 517 | ], 518 | "markers": "python_version >= '3.5'", 519 | "version": "==1.0.0" 520 | }, 521 | "nodeenv": { 522 | "hashes": [ 523 | "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", 524 | "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" 525 | ], 526 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 527 | "version": "==1.8.0" 528 | }, 529 | "packaging": { 530 | "hashes": [ 531 | "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", 532 | "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" 533 | ], 534 | "markers": "python_version >= '3.7'", 535 | "version": "==24.0" 536 | }, 537 | "pampy": { 538 | "hashes": [ 539 | "sha256:304470a6562173096fc88c22dc0ab50401ca2e3875a8725ee510759ec8ba58ff", 540 | "sha256:82054212e4478fc22163c55321a3583eda9918aff4440eed6c197e872a2a667b" 541 | ], 542 | "index": "pypi", 543 | "markers": "python_version >= '3.7'", 544 | "version": "==0.3.0" 545 | }, 546 | "pathspec": { 547 | "hashes": [ 548 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 549 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 550 | ], 551 | "markers": "python_version >= '3.8'", 552 | "version": "==0.12.1" 553 | }, 554 | "platformdirs": { 555 | "hashes": [ 556 | "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", 557 | "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" 558 | ], 559 | "markers": "python_version >= '3.8'", 560 | "version": "==4.3.6" 561 | }, 562 | "pluggy": { 563 | "hashes": [ 564 | "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", 565 | "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" 566 | ], 567 | "markers": "python_version >= '3.8'", 568 | "version": "==1.3.0" 569 | }, 570 | "pre-commit": { 571 | "hashes": [ 572 | "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e", 573 | "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65" 574 | ], 575 | "index": "pypi", 576 | "markers": "python_full_version >= '3.6.1'", 577 | "version": "==2.16.0" 578 | }, 579 | "py": { 580 | "hashes": [ 581 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 582 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 583 | ], 584 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 585 | "version": "==1.11.0" 586 | }, 587 | "pygments": { 588 | "hashes": [ 589 | "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692", 590 | "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29" 591 | ], 592 | "markers": "python_version >= '3.7'", 593 | "version": "==2.16.1" 594 | }, 595 | "pylint": { 596 | "hashes": [ 597 | "sha256:4f4a52b132c05b49094b28e109febcec6bfb7bc6961c7485a5ad0a0f961df289", 598 | "sha256:b4b5a7b6d04e914a11c198c816042af1fb2d3cda29bb0c98a9c637010da2a5c5" 599 | ], 600 | "index": "pypi", 601 | "markers": "python_full_version >= '3.6.2'", 602 | "version": "==2.12.1" 603 | }, 604 | "pytest": { 605 | "hashes": [ 606 | "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", 607 | "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" 608 | ], 609 | "index": "pypi", 610 | "markers": "python_version >= '3.6'", 611 | "version": "==6.2.5" 612 | }, 613 | "pytest-asyncio": { 614 | "hashes": [ 615 | "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b", 616 | "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb" 617 | ], 618 | "index": "pypi", 619 | "markers": "python_version >= '3.6'", 620 | "version": "==0.16.0" 621 | }, 622 | "python-dateutil": { 623 | "hashes": [ 624 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 625 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 626 | ], 627 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 628 | "version": "==2.8.2" 629 | }, 630 | "pyyaml": { 631 | "hashes": [ 632 | "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", 633 | "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", 634 | "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", 635 | "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", 636 | "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", 637 | "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", 638 | "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", 639 | "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", 640 | "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", 641 | "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", 642 | "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", 643 | "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", 644 | "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", 645 | "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", 646 | "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", 647 | "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", 648 | "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", 649 | "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", 650 | "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", 651 | "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", 652 | "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", 653 | "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", 654 | "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", 655 | "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", 656 | "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", 657 | "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", 658 | "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", 659 | "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", 660 | "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", 661 | "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", 662 | "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", 663 | "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", 664 | "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", 665 | "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", 666 | "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", 667 | "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", 668 | "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", 669 | "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", 670 | "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", 671 | "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", 672 | "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", 673 | "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", 674 | "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", 675 | "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", 676 | "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", 677 | "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", 678 | "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", 679 | "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", 680 | "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", 681 | "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" 682 | ], 683 | "markers": "python_version >= '3.6'", 684 | "version": "==6.0.1" 685 | }, 686 | "regex": { 687 | "hashes": [ 688 | "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a", 689 | "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07", 690 | "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca", 691 | "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58", 692 | "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54", 693 | "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed", 694 | "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff", 695 | "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528", 696 | "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9", 697 | "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971", 698 | "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14", 699 | "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af", 700 | "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302", 701 | "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec", 702 | "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597", 703 | "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b", 704 | "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd", 705 | "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767", 706 | "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f", 707 | "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6", 708 | "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293", 709 | "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be", 710 | "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41", 711 | "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc", 712 | "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29", 713 | "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964", 714 | "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d", 715 | "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a", 716 | "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc", 717 | "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55", 718 | "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af", 719 | "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930", 720 | "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e", 721 | "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d", 722 | "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863", 723 | "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c", 724 | "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f", 725 | "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e", 726 | "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d", 727 | "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368", 728 | "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb", 729 | "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52", 730 | "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8", 731 | "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4", 732 | "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac", 733 | "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e", 734 | "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2", 735 | "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a", 736 | "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4", 737 | "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa", 738 | "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533", 739 | "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b", 740 | "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588", 741 | "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0", 742 | "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915", 743 | "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841", 744 | "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a", 745 | "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988", 746 | "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292", 747 | "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3", 748 | "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c", 749 | "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f", 750 | "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420", 751 | "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9", 752 | "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f", 753 | "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0", 754 | "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b", 755 | "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037", 756 | "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b", 757 | "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee", 758 | "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c", 759 | "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b", 760 | "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353", 761 | "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051", 762 | "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039", 763 | "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a", 764 | "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b", 765 | "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e", 766 | "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5", 767 | "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf", 768 | "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94", 769 | "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991", 770 | "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711", 771 | "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a", 772 | "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab", 773 | "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a", 774 | "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11", 775 | "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48" 776 | ], 777 | "markers": "python_version >= '3.7'", 778 | "version": "==2023.10.3" 779 | }, 780 | "requests": { 781 | "hashes": [ 782 | "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5", 783 | "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8" 784 | ], 785 | "index": "pypi", 786 | "markers": "python_version >= '3.8'", 787 | "version": "==2.32.0" 788 | }, 789 | "setuptools": { 790 | "hashes": [ 791 | "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", 792 | "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" 793 | ], 794 | "index": "pypi", 795 | "markers": "python_version >= '3.8'", 796 | "version": "==70.0.0" 797 | }, 798 | "six": { 799 | "hashes": [ 800 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 801 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 802 | ], 803 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 804 | "version": "==1.16.0" 805 | }, 806 | "snowballstemmer": { 807 | "hashes": [ 808 | "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", 809 | "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" 810 | ], 811 | "version": "==2.2.0" 812 | }, 813 | "sphinx": { 814 | "hashes": [ 815 | "sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f", 816 | "sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45" 817 | ], 818 | "markers": "python_version >= '3.6'", 819 | "version": "==4.3.1" 820 | }, 821 | "sphinxcontrib-applehelp": { 822 | "hashes": [ 823 | "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", 824 | "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e" 825 | ], 826 | "markers": "python_version >= '3.8'", 827 | "version": "==1.0.4" 828 | }, 829 | "sphinxcontrib-devhelp": { 830 | "hashes": [ 831 | "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", 832 | "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" 833 | ], 834 | "markers": "python_version >= '3.5'", 835 | "version": "==1.0.2" 836 | }, 837 | "sphinxcontrib-htmlhelp": { 838 | "hashes": [ 839 | "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", 840 | "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903" 841 | ], 842 | "markers": "python_version >= '3.8'", 843 | "version": "==2.0.1" 844 | }, 845 | "sphinxcontrib-jsmath": { 846 | "hashes": [ 847 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 848 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 849 | ], 850 | "markers": "python_version >= '3.5'", 851 | "version": "==1.0.1" 852 | }, 853 | "sphinxcontrib-qthelp": { 854 | "hashes": [ 855 | "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", 856 | "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" 857 | ], 858 | "markers": "python_version >= '3.5'", 859 | "version": "==1.0.3" 860 | }, 861 | "sphinxcontrib-serializinghtml": { 862 | "hashes": [ 863 | "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", 864 | "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" 865 | ], 866 | "markers": "python_version >= '3.5'", 867 | "version": "==1.1.5" 868 | }, 869 | "toml": { 870 | "hashes": [ 871 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 872 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 873 | ], 874 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 875 | "version": "==0.10.2" 876 | }, 877 | "tomli": { 878 | "hashes": [ 879 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 880 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 881 | ], 882 | "markers": "python_version < '3.11'", 883 | "version": "==2.0.1" 884 | }, 885 | "typing-extensions": { 886 | "hashes": [ 887 | "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", 888 | "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" 889 | ], 890 | "markers": "python_version < '3.11'", 891 | "version": "==4.10.0" 892 | }, 893 | "urllib3": { 894 | "hashes": [ 895 | "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", 896 | "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" 897 | ], 898 | "index": "pypi", 899 | "markers": "python_version >= '3.8'", 900 | "version": "==2.2.2" 901 | }, 902 | "virtualenv": { 903 | "hashes": [ 904 | "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", 905 | "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2" 906 | ], 907 | "index": "pypi", 908 | "markers": "python_version >= '3.7'", 909 | "version": "==20.26.6" 910 | }, 911 | "wrapt": { 912 | "hashes": [ 913 | "sha256:01592f7b69b0e721146eed35f0e73f64d06c4caf449d630382863e1f1709150d", 914 | "sha256:09695a747c4af43a50425a45badebdb932d026d6e4d57b885e328f1294a14825", 915 | "sha256:115970769617c7d03d23ecfa08c174f29a9bbf8da08ded204702bc34b76658ab", 916 | "sha256:169ae1817e18a8ae3e10796b1e0c203e04d83ca487f64e7ddb22077eae9a1915", 917 | "sha256:16e5d269660b7217a7a53631df87900ee8937038fae6a3adb88411b108bb8f50", 918 | "sha256:17e978083c577ee724c8eeb0155ed96dd7a1e50b97f30c535da1c09cda0970c9", 919 | "sha256:1e3df5fd073559d32ad8cf6ee10d55e956ed794c130b24c57769416a13c37a60", 920 | "sha256:221eca686d29b2babd09007296816fe338e502a002c965b27737d108e2ce1832", 921 | "sha256:228533d82158a4d25de8b3703195d5c69bae5860010c75bd01f6ab393561ea22", 922 | "sha256:22c6ba8b141ec9b6e34beff8429bd03cd082987dd4831d288b2931fee6c34d29", 923 | "sha256:2522e09d2f8fc0f1db4aafdeebe84597c078cd7d70cc56b1bbc4887870b1f24c", 924 | "sha256:29c20ca9bab38759a1d8fb171ad17f7da5ec18855fc7aacd6c93edc83dbc9764", 925 | "sha256:29d70cbf5814bf91193cdac1ffc76240adfeb7c89a8070dc29e49674249542c2", 926 | "sha256:2e287347a241c3db125d58ec0c0c9f363fd7cbfdf2affc352c0f5bd4d0f0b799", 927 | "sha256:378021d27d168f6f09cf69eaaa9052c8ea48e103beb7d403fc978ad7d6dcdc3e", 928 | "sha256:399a64e99b3a327159e79cc20b6f0552e72e32bf6155d5521b0496cab26d1110", 929 | "sha256:3a7e40704f48038f5233b993d61ac3195beb748cfe8e8f4698349c5fecbe6280", 930 | "sha256:3b13c53d8629cced58bdee9ee3c3a4372097660dad7e8398b3f26267a942cb2e", 931 | "sha256:3ca9ab90a5d6fa91761a6151018819a9239b28c9cc2e8e1d679833bd0f5f939c", 932 | "sha256:3cf2fae5a9757491ed8756f580ece8312fea95e07be2b340ddef982a766a7ac7", 933 | "sha256:3ee64ab62c8fdc671c16b6d44bee12d598e0ad936b3a17e891404fd79f1f3d61", 934 | "sha256:4148753415a77f674a9f3ed372d68f9b6c0e83e6f2bcf91d3068462a40cb43cd", 935 | "sha256:4409bd44e5ffa1656bce5c8824155abc0e3151d07393c3434a28f812c5a123d0", 936 | "sha256:467ca88d704655f3e7429af2fa4649d602cb27ce289f26b833c207bf11d96830", 937 | "sha256:4c51ab4213d97f9e2287c5dd9f61fc44a2604be79246239ebadeae636b274596", 938 | "sha256:4e07ac0a4f44693bfc8df134b181268adfcdd7e73db9814901df3e051d9266fc", 939 | "sha256:4f31b53e611854ab300535f81038a83dbcd472814dd3a9970eeb201578df6262", 940 | "sha256:54be66645d5b2358b9294ff0349005789541ac9a4c3a10d60042685e2ea51ca0", 941 | "sha256:55d9cbee700697ae3a5a34045446d0890baada178fe6028604e8f2c9992470cb", 942 | "sha256:5816dee9ab69a18ca47a0d1d67086b2995910e11b7a0a2a2bae6e9ac63ac2828", 943 | "sha256:59ecece1b2b35d5fc6f1605d15e90d4342b0658b17158643e6a17b72e38da826", 944 | "sha256:5b3f2cef94b53f0d55d96c6309f1100110475c7b1fb907bc5f3fd2b0c940b238", 945 | "sha256:604dad6f6b34d767097b12a1ae84a128d899626c78e86e1180eff35d64f1a57d", 946 | "sha256:640fdfcb41865941c2fa4c0dfb9db6ab389d65e3266a464afeeff23f8f77fb24", 947 | "sha256:65586e7a33267e5725cf228c0f7b9e819ab60c0c88ccf9827c2e0526d43a1103", 948 | "sha256:6bb06bfe4c53f65d59bbe9101a8181c5e8dcde1ad0f778a4d502b599ebc213e6", 949 | "sha256:6d4950d0b5ebbe74ca549b2000474330988c88ae59a67b961e2cfdc18cd75003", 950 | "sha256:706bddba81c86330d92bbf080afef4a8c4f4fd86e0bc4a1975fac9b84c6758af", 951 | "sha256:76263e0c1207dfe9099805d1cf6147de05f638a598c8453cd8bb914aaf7520db", 952 | "sha256:77cd63017a8c35ead1a07f85c3ec4fa259bb2260332d69c6e9ae4c35b2c8e79f", 953 | "sha256:793c91e9c86d80850f2f40f1d3d5dce4f810f5aa6e5a80310fb8d32f5210f4df", 954 | "sha256:7d71ac38f1f178a8d3139e5560d95dabd4f894820a5627d0cd7535e9a255056a", 955 | "sha256:7e372d054af5b9652c7ba05fda93aec3bcb8f53dab21e61249c98a1596e48402", 956 | "sha256:8c9b972bc3dad5363f966912b8134b2ebfc1dc5a030e0835dc6c60f107390fe7", 957 | "sha256:969d518cc42be5f9f78fb7e6f42e51796e1ebce02219c93a03fcb29a7a3eb1e7", 958 | "sha256:a3461740b336424b836840f3568a56775e5fc988521cb6072aa3c3f2a589036c", 959 | "sha256:a3a15c874a1a30a9c4edb5ec55d96c1210f5974df51a6d69a367aace74378467", 960 | "sha256:a3fdde5f045de444875fa2c6822a1551dd03dcbb3a22fb52ded73744d7ebad55", 961 | "sha256:a7675ff09d87435b8f678d17e78211cb589ae805fd31579dc918e23c71710a6c", 962 | "sha256:a9a65f98a571083dc61419295aeb8d59170227227d4ba13d6d5b96a953a519aa", 963 | "sha256:b265aaab64dfb9db412ee315ff23f52f1986cf6e0989d719d90423baf4019d63", 964 | "sha256:b96bbcbf81ee9ee2e0c81a5f2a3bd0975a6dc0a6a9fc335f9b302a661999e3ed", 965 | "sha256:bdbb31db39b69d0f0e5ae83f99b8e28fa3ad7b7e05de6c5faa3cd52a4de20ef5", 966 | "sha256:c3f3c320272601223a036fecd942dd1258a15cebe88e18012cc88b2e6b813483", 967 | "sha256:c85b8fbd7c0e303a6d6e5731a7898f10e070dead30822b0481327dba74e7123c", 968 | "sha256:c939d2cc4015311e8aae68f52a6bc8e69c02b2c7166953fef9c1f06657aabdfc", 969 | "sha256:d6a866d5b8fc0f713440aafb9507f688f4d660c2f868093ae6cd0acb62d1a918", 970 | "sha256:d7c7e203e93f1eed57880f84505e7d2b4ece02e3ed7c6690ba90d0385f1a74b4", 971 | "sha256:d8d2fda2907c42ba3df720f0c7a704f36e09c586c8f7a4eed8b9db0e1d2fa79a", 972 | "sha256:db39f9065ed5b30081f8df71ced66cd29640b21e0091e2e5572ba0d70078f611", 973 | "sha256:db4772a9498023ce19f95b7aa86a8d94c8838269597361a986133373990be41f", 974 | "sha256:dd5dd67c074201664e4b80022128ef6eee8a007f2281d7099cd2114061af796a", 975 | "sha256:dfc2a91a23de91c9cedc9bc34742469ad7d1f177d4bf1a7a359af1adf9050e9b", 976 | "sha256:e5b450194731714cba9267e5c04e1f3622090c3ec43e1acba36744d92354da5e", 977 | "sha256:e7b324089cb59d700ed5cace6b39013f81473d1bf410da77a0d304ebadbc6eb9", 978 | "sha256:ea6041327840465f2450a1c8894a834a99aa4626a96f82547d9c042526eb1487", 979 | "sha256:eb72f906837cea3583fd9c91c6e286f8616360767703c837e7dfa1a70b123ad0", 980 | "sha256:f0b9edc564b1af9e9ac9cf932349136c74894ce2f699e00c1279b0fa5909d515", 981 | "sha256:f9e10c3bf07074377fbbff3d2b02d740c17602ce5d6c91455977bdb32fbbebe8", 982 | "sha256:fc43eb869c6baba54dda3264109354a5d0ea621c51ba945ff71308347f24a1c9" 983 | ], 984 | "markers": "python_version >= '3.6'", 985 | "version": "==1.16.0rc1" 986 | }, 987 | "zipp": { 988 | "hashes": [ 989 | "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091", 990 | "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f" 991 | ], 992 | "index": "pypi", 993 | "markers": "python_version >= '3.8'", 994 | "version": "==3.19.1" 995 | } 996 | } 997 | } 998 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PyPI](https://img.shields.io/pypi/v/pytask-io) 2 | ![Read the Docs](https://img.shields.io/readthedocs/pytask-io) 3 | ![GitHub](https://img.shields.io/github/license/joegasewicz/pytask_io) 4 | 5 | 6 | Asynchronous Tasks Library using asyncio 7 | 8 | An Asyncio based task queue that is designed to be super easy to use! 9 | 10 | Read the docs: [Documentation](https://pytask-io.readthedocs.io/en/latest/) 11 | 12 | ![PyTask IO](assets/Group.png?raw=true "Title") 13 | 14 | ## Install 15 | ```bash 16 | pip install pytask-io 17 | 18 | docker run redis # Rabbit MQ coming soon... 19 | 20 | ``` 21 | 22 | 23 | ### Usage 24 | 25 | ```python 26 | from pytask_io import PyTaskIO 27 | 28 | # Starts the task runner 29 | pytask = PytaskIO( 30 | store_port=6379, 31 | store_host="localhost", 32 | db=0, 33 | workers=1, 34 | ) 35 | 36 | # Start the PytaskIO task queue on a separate thread. 37 | pytask.run() 38 | 39 | # Handle a long running process, in this case a send email function 40 | metadata = pytask.add_task(send_email, title, body) 41 | 42 | # Try once to get the results of your email sometime in the future 43 | result = pytask.get_task(metadata) 44 | 45 | # Later we can use the `metadata` result to pass to `add_task` 46 | result = pytask.poll_for_task(metadata, tries=100, interval=60) 47 | 48 | # Stop PytaskIO completely (This will not affect any units of work that haven't yet executed) 49 | pytask.stop() 50 | 51 | ``` 52 | ### Compatible task types 53 | PyTaskIO will always return back the task meta data. 54 | Do not embed Python objects of type frame, generator, traceback & context objects. 55 | In this case you will get back the exception thrown when PyTaskIO attempts to execute the serialization. 56 | 57 | 58 | 59 | ## Authors 60 | 61 | * **joegasewicz** - *Initial work* - [@joegasewicz](https://twitter.com/joegasewicz) 62 | 63 | ## Contributing 64 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 65 | 66 | Please make sure to update tests as appropriate. 67 | 68 | ## License 69 | [MIT](https://choosealicense.com/licenses/mit/) 70 | -------------------------------------------------------------------------------- /assets/Group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joegasewicz/pytask-io/6aadc53800502fbd596d9be15e8d3545a87df87a/assets/Group.png -------------------------------------------------------------------------------- /assets/pytaskio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joegasewicz/pytask-io/6aadc53800502fbd596d9be15e8d3545a87df87a/assets/pytaskio.jpg -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | sys.setrecursionlimit(1500) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'Pytask IO' 22 | copyright = '2020, Joe Gasewicz' 23 | author = 'Joe Gasewicz' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '0.0.1' 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc'] 35 | autodoc_mock_imports = ["jwt"] 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'alabaster' 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ['../_static'] -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Pytask IO documentation master file, created by 2 | sphinx-quickstart on Sun Feb 9 20:21:16 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Pytask IO's documentation! 7 | ===================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | pytask_io 14 | 15 | 16 | Features 17 | +++++++++++++++++++++++++++++++++++++++ 18 | * Asynchronous Tasks Library using asyncio 19 | * Asyncio based task queue that is designed to be super easy the use! 20 | 21 | 22 | Installation:: 23 | 24 | pip install pytask-io -------------------------------------------------------------------------------- /docs/source/pytask_io.rst: -------------------------------------------------------------------------------- 1 | Pytask IO 2 | ========= 3 | .. automodule:: pytask_io.pytask_io 4 | :members: 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta:__legacy__" -------------------------------------------------------------------------------- /pytask_io/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | - Concepts 3 | - PYTASKIO / CLIENT: Is the client. 4 | - WORKERS: Processes to handle unit of work 5 | - QUEUE: where the tasks are stored. 6 | - EVENT LOOP: the process which tasks are run on. 7 | - STORE: Redis is the store used to store data returned from tasks. 8 | 9 | - Abstract 10 | - A client issues a task to be enqueued on the QUEUE. 11 | - A worker pulls tasks off the queue, deserializes & runs them on an asyncIO EVENT LOOP. 12 | 13 | - Setup 14 | - Redis is the message broker. 15 | 16 | """ 17 | from .pytask_io import PyTaskIO 18 | 19 | if __name__ == "__main__": 20 | pass 21 | -------------------------------------------------------------------------------- /pytask_io/actions.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class QueueActions(Enum): 5 | START = 1 6 | STOP = 2 7 | IDLE = 3 8 | -------------------------------------------------------------------------------- /pytask_io/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import redis 3 | from typing import Callable, Any, List 4 | import threading 5 | 6 | from pytask_io.worker import worker 7 | from pytask_io.utils import get_task_from_queue_client, deserialize_task 8 | from pytask_io.actions import QueueActions 9 | from pytask_io.logger import logger 10 | 11 | tasks = [] 12 | 13 | 14 | async def client(worker_queue: asyncio.Queue, queue_client: redis.Redis, workers_required: int): 15 | """ 16 | Recursive client for Workers, Tasks & AsyncIO's Event Loop that 17 | reacts on QueueAction names. 18 | :param worker_queue: 19 | :param queue_client: 20 | :param workers_required: 21 | :return: 22 | """ 23 | queue = worker_queue 24 | next_task = await get_task_from_queue_client(queue_client) 25 | action_name = "" 26 | try: 27 | action_name: str = next_task[1].decode("utf-8") 28 | logger.debug(f"QueueAction name: {action_name}") 29 | except UnicodeDecodeError: 30 | pass 31 | 32 | if action_name == QueueActions.START.name: 33 | for i in range(workers_required): 34 | task = asyncio.create_task(worker(queue, queue_client)) 35 | tasks.append(task) 36 | await client(worker_queue, queue_client, workers_required) 37 | 38 | elif action_name == QueueActions.STOP.name: 39 | await queue.join() 40 | for task in tasks: 41 | task.cancel() 42 | # Wait until all worker tasks are cancelled 43 | await asyncio.gather(*tasks, return_exceptions=True) 44 | event_loop = asyncio.get_running_loop() 45 | event_loop.stop() 46 | 47 | elif action_name == QueueActions.IDLE.name: 48 | # We have the option to stop & close the event loop here 49 | # OR pass execution context to another aspect of the library 50 | pass 51 | 52 | else: 53 | uow_metadata = await deserialize_task(next_task[1]) 54 | serialized_uow: List[Callable, List[Any]] = await deserialize_task(uow_metadata["serialized_uow"]) 55 | unit_of_work = { 56 | "function": serialized_uow[0], 57 | "args": serialized_uow[1], 58 | } 59 | 60 | uow_metadata["unit_of_work"] = unit_of_work 61 | # Push unit of work metada dict into the asyncio queue 62 | queue.put_nowait(uow_metadata) 63 | await client(worker_queue, queue_client, workers_required) 64 | -------------------------------------------------------------------------------- /pytask_io/event_loop.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Callable 3 | 4 | 5 | def event_loop(client: Callable): 6 | """The event loop""" 7 | asyncio.run(client()) 8 | -------------------------------------------------------------------------------- /pytask_io/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyTaskIOException(Exception): 2 | pass 3 | 4 | class NotReadyException(PyTaskIOException): 5 | message = "Call `run()` first on your PyTaskIO instance." 6 | 7 | def __init__(self, message=None): 8 | super().__init__(self.message or message) -------------------------------------------------------------------------------- /pytask_io/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | 5 | def set_log_level(): 6 | """ 7 | Example:: 8 | export PYTASKIO_DEBUG=1 9 | :return: 10 | """ 11 | PYTASKIO_DEBUG: str = os.getenv("PYTASKIO_DEBUG") or "0" 12 | if int(PYTASKIO_DEBUG) == 1: 13 | logging.basicConfig(level=logging.DEBUG) 14 | else: 15 | logging.basicConfig(level=logging.INFO) 16 | return logging.getLogger(__name__) 17 | 18 | 19 | logger = set_log_level() 20 | -------------------------------------------------------------------------------- /pytask_io/pytask_io.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pytask IO Class 3 | =============== 4 | """ 5 | import asyncio 6 | from typing import Callable 7 | import redis 8 | from threading import Thread 9 | from typing import Dict, Any, Union 10 | import threading 11 | import time 12 | 13 | from pytask_io.task_queue import ( 14 | poll_for_store_results, 15 | ) 16 | from pytask_io.store import ( 17 | init_unit_of_work, 18 | get_uow_from_store, 19 | push_action_name, 20 | ) 21 | from pytask_io.logger import logger 22 | from pytask_io.client import client 23 | from pytask_io.actions import QueueActions 24 | 25 | 26 | class PyTaskIO: 27 | """ 28 | :kwargs: 29 | :key store_host: The store host name. Default is `localhost`. 30 | :key store_port: The store port. Default is 0 31 | :key store db: The store db number. Default is 6379 32 | :key workers: The amount of workers in the asyncio task queue. Default is 1. 33 | """ 34 | 35 | #: PytaskIO is a python task queue that leverages CPython's asyncio library 36 | #: to make long running task trivial. The library aims to make the public 37 | #: API as simple and intuitive as possible. 38 | #: Basic usage. Example:: 39 | #: 40 | #: Starts the task runner 41 | #: pytask = PytaskIO( 42 | #: store_port=8080, 43 | #: store_host="localhost", 44 | #: broker="redis", # rabbitmq coming soon... 45 | #: db=0, 46 | #: ) 47 | #: 48 | #: # Start the PytaskIO task queue on a separate thread. 49 | #: pytask.run() 50 | #: 51 | #: # Handle a long running process, in this case a send email function 52 | #: metadata = pytask.add_task(send_email, title, body) 53 | #: 54 | #: # Try once to get the results of your email sometime in the future 55 | #: result = get_task(metadata) 56 | #: 57 | #: # Stop PytaskIO completly (This will not effect any units of work that havent yet executed) 58 | #: pytask.stop() 59 | #: 60 | #: The connected queue client object. Use this object exactly as you would if you were referencing 61 | #: the queue's client directly. Example:: 62 | #: 63 | #: # Example for default Redis queue pushing a task into the queue 64 | #: pytaskio = PytaskIO() 65 | #: results = pytaskio.queue_client.lpush("my_queue", my_task) 66 | queue_client: redis.Redis = None 67 | 68 | #: The `queue_store` is available to work with & can be accessed on the PyTaskIO instance 69 | #: & all the available methods from the store framework used will be available on this object. 70 | #: Foe example:: 71 | #: 72 | #: # Example fpr default Redis store setting a new key 73 | #: pytaskio = PytaskIO() 74 | #: pytaskio.queue_store.set('myfield', 'my_value') 75 | queue_store: redis.Redis = None 76 | 77 | _worker_queue: asyncio.Queue = None 78 | 79 | #: The thread that the asyncio event loop runs in. This thread has been tagged with the name of 80 | #: `event_loop`. The thread will die gracefully when PytaskIO calls `event_loop.join()` for you. If 81 | #: you require custom handling of the `event_loop` thread, then you can access it directly using 82 | #: the :class:`pytask_io.loop_thread` object. Example:: 83 | #: 84 | #: pytaskio = PytaskIO() 85 | #: pytaskio.loop_thread.is_alive() # check if the thread is still alive 86 | loop_thread: Thread = None 87 | 88 | #: The main loop that is used by PytaskIO. If you wish to handle some of the asyncio behavior of the 89 | #: main loop, then you can access the asyncio object directly with :class:`pytask_io.main_loop`. 90 | main_loop: asyncio.AbstractEventLoop = None 91 | 92 | #: The pole loop that is available for PytaskIO public methods such as :class:`pytask_io.poll_for_task` 93 | #: If you wish to handle some of the asyncio behavior of the pole loop, then you can access the asyncio 94 | #: object directly with :class:`pytask_io.pole_loop`. 95 | pole_loop: asyncio.AbstractEventLoop 96 | 97 | #: The store host name. Default is `localhost`. 98 | store_host: str = "localhost" 99 | 100 | #: The store port. Default is 0 101 | store_port: int = 6379 102 | 103 | #: The store db number. Default is 6379 104 | store_db: int = 0 105 | 106 | #: The amount of workers in the asyncio task queue. Default is 1. 107 | workers: int = 1 108 | 109 | _polled_result: Dict = None 110 | 111 | def __init__(self, *args, **kwargs): 112 | self.store_host = kwargs.get("store_host") or self.store_host 113 | self.store_port = kwargs.get("store_port") or self.store_port 114 | self.store_db = kwargs.get("store_db") or self.store_db 115 | self.workers = kwargs.get("workers") or self.workers 116 | 117 | def init(self, **kwargs): 118 | """ 119 | :param kwargs: 120 | :return: 121 | """ 122 | self.store_host = kwargs.get("store_host") or self.store_host 123 | self.store_port = kwargs.get("store_port") or self.store_port 124 | self.store_db = kwargs.get("store_db") or self.store_db 125 | self.workers = kwargs.get("workers") or self.workers 126 | 127 | def run(self): 128 | """ 129 | Starts an event loop on a new thread with a name of `event_loop` 130 | Example:: 131 | 132 | # Create a `pytaskio` object & run the event loop in new thread: 133 | pytaskio = PyTaskIO() 134 | pytaskio.run() 135 | 136 | :return: 137 | """ 138 | self.queue_client = self._connect_to_store() 139 | self.queue_store = self._connect_to_store() 140 | _ = push_action_name(self.queue_client, QueueActions.START.name) 141 | self.loop_thread = Thread( 142 | name="event_loop", 143 | target=self._run_event_loop, 144 | ) 145 | self.loop_thread.daemon = True 146 | self.loop_thread.start() 147 | logger.info("PyTaskIO running...") 148 | 149 | def _connect_to_store(self) -> redis.Redis: 150 | """ 151 | Private method that is used to connect to the client queue & store. 152 | This will change when we introduce more store & queue options. 153 | :return: None 154 | """ 155 | return redis.Redis( 156 | host=self.store_host, 157 | port=self.store_port, 158 | db=self.store_db 159 | ) 160 | 161 | def stop(self) -> int: 162 | """ 163 | Method to elegantly stop the asyncio event loop & any associated threads. 164 | This method will only be executed when all active task have finished executing. 165 | If there are any pending tasks left in the clients queue then these can be executed 166 | once PytaskIO is run again. 167 | Example:: 168 | 169 | pytaskio = PyTaskIO() 170 | pytaskio.run() 171 | try: 172 | metadata = pytask.add_task(send_email, title, body) 173 | except RunTimeError: 174 | res = pytaskio.stop() 175 | print(res) # index of queue work item 176 | 177 | :return: The queue index 178 | """ 179 | res = push_action_name(self.queue_client, QueueActions.STOP.name) 180 | while any(t.is_alive() and t.getName() == "event_loop" for t in threading.enumerate()): 181 | time.sleep(0.25) 182 | return res 183 | 184 | def _run_event_loop(self, action: str = None) -> None: 185 | """ 186 | :return: None 187 | """ 188 | self.main_loop = asyncio.new_event_loop() 189 | self._worker_queue = asyncio.Queue() 190 | self.main_loop.create_task(client(self._worker_queue, self.queue_client, self.workers)) 191 | self.main_loop.run_forever() 192 | 193 | def add_task(self, unit_of_work: Callable, *args) -> Dict[str, Any]: 194 | """ 195 | :class:`pytask_io.add_task` method take a function as a first argument 196 | & the function arguments as the next arguments. When :class:`pytask_io.add_task` 197 | is called, it will return a dictionary with some useful data. Although the data 198 | returned can be used in many ways, the easiest & most straight forward use is to 199 | pass this dict directly to :class:`pytask_io.get_task`. 200 | Example:: 201 | 202 | # Create a `pytaskio` object & run the event loop in new thread: 203 | pytaskio = PyTaskIO() 204 | pytaskio.run() 205 | 206 | # the `add_task` method task in a function as a first argument 207 | metadata = pytask.add_task(send_email, title, body) 208 | 209 | 210 | # Later we can use the `metadata` result to pass to `add_task` 211 | result = get_task(metadata) 212 | 213 | :param unit_of_work: A callable / executable Python function 214 | :param args: The list of arguments required by `unit_of_work` 215 | :return: metadata 216 | """ 217 | return init_unit_of_work(self.queue_client, self.queue_store, unit_of_work, *args) 218 | 219 | def get_task(self, unit_of_work_metadata: Dict[str, Any]) -> Union[Dict[str, Any], None]: 220 | """ 221 | Method to get the task results from the store. This function can be called 222 | directly after executing the :class:`pytask_io.add_task`. If the results 223 | referenced from the `metadata` dict *see below* are available when this method 224 | is called, then the result is return otherwise `None` is returned. If the 225 | result is `None`, then you have to retry to call this method again, sometime 226 | in the future. 227 | Example:: 228 | 229 | # Create a `pytaskio` object & run the event loop in new thread: 230 | pytaskio = PyTaskIO() 231 | pytaskio.run() 232 | 233 | # the `add_task` method task in a function as a first argument 234 | metadata = pytask.add_task(send_email, title, body) 235 | 236 | 237 | # Later we can use the `metadata` result to pass to `add_task` 238 | result = get_task(metadata) 239 | 240 | :param unit_of_work_metadata: Dict[str, Any] - 241 | :return Union[Dict, bool]: The result is non blocking 242 | """ 243 | result = get_uow_from_store(self.queue_store, unit_of_work_metadata["store_name"]) 244 | if result == "": 245 | return None 246 | return result 247 | 248 | def poll_for_task(self, task_meta: Dict, **kwargs) -> Union[Dict[str, Any], bool]: 249 | """ 250 | Warning: This method is still in a beta state, use with care! 251 | Blocking function to be used either with an async library or from a separate thread 252 | from the client application's main thread. 253 | This method will create an asyncio event loop & make periodic requests to the store. 254 | Once the results are returned, the loop with be stopped and the main thread will 255 | be available once again. 256 | Example:: 257 | 258 | # Create a `pytaskio` object & run the event loop in new thread: 259 | pytaskio = PyTaskIO() 260 | pytaskio.run() 261 | 262 | # the `add_task` method task in a function as a first argument 263 | metadata = pytask.add_task(send_email, title, body) 264 | 265 | 266 | # Later we can use the `metadata` result to pass to `add_task` 267 | result = poll_for_task(metadata, tries=100, interval=60) 268 | 269 | :param task_meta: 270 | :param kwargs: 271 | :key tries: The amount of times the method polls the store for the task execution results. 272 | :key interval: The time in seconds for each try 273 | :return: 274 | """ 275 | tries = kwargs.get("tries") 276 | interval = kwargs.get("interval") 277 | if tries: 278 | # Create event loop in the main thread 279 | self.pole_loop = asyncio.get_event_loop() 280 | # Coroutine to pole store on event loop 281 | get_store_results = poll_for_store_results(self.queue_store, task_meta, interval, tries) 282 | asyncio.set_event_loop(self.pole_loop) 283 | self._polled_result = self.pole_loop.run_until_complete(get_store_results) 284 | if self._polled_result: 285 | task_result_data = { 286 | "data": self._polled_result, 287 | **task_meta, 288 | } 289 | return task_result_data 290 | -------------------------------------------------------------------------------- /pytask_io/store.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from typing import Any, Dict 3 | from pytask_io.logger import logger 4 | 5 | from pytask_io.utils import ( 6 | serialize_store_data, 7 | get_datetime_now, 8 | serialize_unit_of_work, 9 | deserialize_store_data_sync, 10 | ) 11 | from pytask_io.actions import QueueActions 12 | 13 | _QUEUE_NAME = "pytaskio_queue" 14 | 15 | 16 | def _create_uow_metadata(uow_store_name: str, index: int, datetime_now: Any, serialized_uow: bytes): 17 | """ 18 | Creates the main dict data structure for the library 19 | :param uow_store_name: 20 | :param index: 21 | :param datetime_now: Sets the datetime for the store 22 | :return: 23 | """ 24 | uow_metadata = { 25 | "store_type": "redis", 26 | "store_name": uow_store_name, 27 | "store_index": index, 28 | "store_db": 0, 29 | "store_created": datetime_now, 30 | "store_updated": "", 31 | "queue_type": "redis", 32 | "queue_name": _QUEUE_NAME, 33 | "queue_length": "", 34 | "queue_db": 0, 35 | "queue_created": "", 36 | "queue_updated": "", 37 | "unit_of_work": {}, 38 | "serialized_uow": serialized_uow, 39 | "serialized_result": "", 40 | "result_exec_date": "" 41 | } 42 | return uow_metadata 43 | 44 | 45 | def _create_store_index(store: Any): 46 | store.incr("task_auto_index") 47 | current_index = store.get("task_auto_index").decode("utf-8") 48 | return current_index 49 | 50 | 51 | def _create_store_key(current_index, store, serialized_data: bytes = None) -> Dict[str, Any]: 52 | uow_store_name = f"uow_result_#{current_index}" 53 | init_success = store.set(uow_store_name, 0) 54 | if not init_success: 55 | logger.error("PyTaskIO Error: Store was unsuccessful creating registry for unit of work.") 56 | uow_metadata = _create_uow_metadata( 57 | uow_store_name, 58 | current_index, 59 | get_datetime_now(), 60 | serialized_data, 61 | ) 62 | 63 | serialized_uow_meta = serialize_store_data(uow_metadata) 64 | update_success = store.set(uow_store_name, serialized_uow_meta) 65 | if not update_success: 66 | logger.error("PyTaskIO Error: Store was unsuccessful updating meta for unit of work.") 67 | return uow_metadata 68 | 69 | 70 | def init_unit_of_work(q, store, unit_of_work, *args) -> Dict[str, Any]: 71 | """ 72 | Function used directly in PyTaskIO class. 73 | :param q: The client task queue 74 | :param unit_of_work: python code to run 75 | :param args: Unit of work arguments 76 | :return: 77 | """ 78 | serialized_uow = serialize_unit_of_work(unit_of_work, *args) 79 | # The unit of work gets saved to the store in case the task 80 | # fails and the client / user wants to retry 81 | uow_metadata = _create_store_key( 82 | _create_store_index(store), 83 | store, 84 | serialized_uow 85 | ) 86 | 87 | # Unit of work gets push onto the task queue & metadata gets updated 88 | serialized_uow_meta = serialize_store_data(uow_metadata) 89 | # LPUSH returns the queue length after the push operation 90 | queue_length = q.lpush(_QUEUE_NAME, serialized_uow_meta) 91 | uow_metadata["queue_created"] = get_datetime_now() 92 | uow_metadata["queue_length"] = queue_length 93 | # Returns metadata back to the caller / user. 94 | return uow_metadata 95 | 96 | 97 | def get_uow_from_store(store, uow_key: str) -> Dict[str, Any]: 98 | """ 99 | Synchronous client version of get_uow_from_store 100 | :param store: 101 | :param uow_key: 102 | :return: 103 | """ 104 | uow_store_metadata = store.get(uow_key) 105 | # TODO uow_store_result is always '' 106 | result = deserialize_store_data_sync(uow_store_metadata) 107 | 108 | if not result: 109 | raise ValueError( 110 | f"[PYTASKIO ValueError]: Could not get unit of work with " 111 | f"key of {uow_key} from store. Did you pass the correct " 112 | f"value to `PyTaskIO.get_task` ?" 113 | ) 114 | else: 115 | return result 116 | 117 | 118 | async def add_uof_result_to_store(executed_uow: Any, uow_metadata: Dict[str, Any], store) -> None: 119 | now = get_datetime_now() 120 | 121 | # Add serialized results to store 122 | uow_metadata["serialized_result"] = executed_uow 123 | uow_metadata["store_updated"] = now 124 | uow_metadata["result_exec_date"] = now 125 | 126 | serialized_metadata = serialize_store_data(uow_metadata) 127 | 128 | update_success = store.set(uow_metadata["store_name"], serialized_metadata) 129 | if not update_success: 130 | logger.error("PyTaskIO Error: Store was unsuccessful updating meta for unit of work.") 131 | 132 | 133 | def push_action_name(q: redis.Redis, action: str) -> int: 134 | """ 135 | :param q: redis.Redis 136 | :param action: QueueAction prop 137 | :return: 138 | """ 139 | res = q.lpush(_QUEUE_NAME, action) 140 | return res 141 | -------------------------------------------------------------------------------- /pytask_io/task_queue.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import redis 3 | from typing import Dict 4 | 5 | 6 | from pytask_io.utils import deserialize_store_data 7 | 8 | 9 | async def poll_for_store_results(queue_store: redis.Redis, task_meta: Dict, tries: int, interval: int): 10 | """ 11 | Streams back results to 12 | :param queue_store: 13 | :param task_meta: 14 | :param tries: 15 | :param interval: 16 | :return: 17 | """ 18 | list_name = task_meta.get("list_name") 19 | task_index = task_meta.get("task_index") 20 | dumped = None 21 | if interval: 22 | while tries > 0: 23 | current_loop = asyncio.get_running_loop() 24 | 25 | result = await current_loop.run_in_executor(None, queue_store.lindex, *[list_name, task_index]) 26 | if result: 27 | dumped = await deserialize_store_data(result) 28 | tries -= 1 29 | break 30 | elif not result: 31 | break 32 | else: 33 | await asyncio.sleep(interval) 34 | 35 | return dumped 36 | -------------------------------------------------------------------------------- /pytask_io/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import redis 3 | import dill 4 | from typing import List, Any, Tuple, Callable 5 | from datetime import datetime 6 | from warnings import warn 7 | 8 | 9 | def serialize_unit_of_work(unit_of_work: Any, *args) -> bytes: 10 | """ 11 | Serializes a unit of work & returns the results 12 | This will return a new function with the error back to the user 13 | with a warning. 14 | :param unit_of_work 15 | :param args: 16 | :return: 17 | """ 18 | try: 19 | serialized_uow = dill.dumps((unit_of_work, [*args])) 20 | return serialized_uow 21 | except TypeError as err: 22 | warn( 23 | "[PYTASK_IO WARNING] Task could not be serialized! " 24 | "PyTaskIO not support frame, generator, traceback & context objects." 25 | ) 26 | def pytask_io_err(err): 27 | return err 28 | return dill.dumps((pytask_io_err, err)) 29 | 30 | 31 | def serialize_store_data(store_data: Any) -> bytes: 32 | """ 33 | Serializes a unit of work & returns the results 34 | :param unit_of_work:s 35 | :param args: 36 | :return: 37 | """ 38 | serialized_uow = dill.dumps((store_data)) 39 | return serialized_uow 40 | 41 | 42 | async def get_task_from_queue_client(q: redis.Redis) -> Tuple[Callable, List]: # TODO correct return type 43 | try: 44 | current_loop = asyncio.get_running_loop() 45 | except RuntimeError as err: 46 | raise RuntimeError(f"PyTaskIO: {err}") 47 | result = await current_loop.run_in_executor(None, q.brpop, "pytaskio_queue") # TODO pytaskio_queue -get from global 48 | return result 49 | 50 | 51 | async def deserialize_task(task_data: Any): 52 | try: 53 | current_loop = asyncio.get_event_loop() 54 | except RuntimeError as err: 55 | raise RuntimeError(f"PyTaskIO: {err}") 56 | try: 57 | result = await current_loop.run_in_executor(None, dill.loads, task_data) 58 | return result 59 | except RuntimeError as err: 60 | raise Exception(err) 61 | 62 | 63 | 64 | async def deserialize_store_data(task_data: Any): 65 | try: 66 | current_loop = asyncio.get_running_loop() 67 | except RuntimeError as err: 68 | raise RuntimeError(f"PyTaskIO: {err}") 69 | if task_data: 70 | result = await current_loop.run_in_executor(None, dill.loads, task_data) 71 | return result 72 | else: 73 | return None 74 | 75 | 76 | def deserialize_store_data_sync(task_data: Any): 77 | """ 78 | Synchronous version of deserialize_store_data 79 | :param task_data: 80 | :return: 81 | """ 82 | deserialized_uow = dill.loads(task_data) 83 | return deserialized_uow 84 | 85 | 86 | def get_datetime_now(): 87 | now = datetime.now() 88 | return now.strftime("%d/%m/%y %H:%M:%S") 89 | -------------------------------------------------------------------------------- /pytask_io/worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import List, Callable 3 | 4 | from pytask_io.store import add_uof_result_to_store 5 | 6 | tasks = [] 7 | 8 | 9 | def get_tasks(task: List[Callable]): 10 | return task 11 | 12 | 13 | async def worker(q: asyncio.Queue, queue_client): 14 | """ 15 | TODO - queue_client needs to be replaced with queue_store as currently this is how this value is 16 | used in add_uof_result_to_store 17 | - Worker 18 | - Observes task queue. 19 | - Fetches available task to run. 20 | - Tasks are run asynchronously on a single asyncIO event loop. 21 | """ 22 | while True: 23 | 24 | uow_metadata = await q.get() 25 | 26 | current_loop = asyncio.get_running_loop() 27 | 28 | # Execute the unit of work & pass in the args 29 | executed_uow = await current_loop.run_in_executor( 30 | None, 31 | uow_metadata["unit_of_work"]["function"], 32 | *uow_metadata["unit_of_work"]["args"], 33 | ) 34 | 35 | await add_uof_result_to_store(executed_uow, uow_metadata, queue_client) 36 | q.task_done() 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | 7 | setup( 8 | name="pytask-io", 9 | version="0.0.10", 10 | description="An asynchronous Tasks Library using asyncio", 11 | packages=['pytask_io'], 12 | install_requires=[ 13 | 'redis>=3.3.11,<5.0.0', 14 | 'dill>=0.3.1.1,<0.4.0.0', 15 | ], 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "Operating System :: OS Independent", 21 | ], 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | url="https://github.com/joegasewicz/pytask_io", 25 | author="Joe Gasewicz", 26 | author_email="joegasewicz@gmail.com", 27 | ) 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joegasewicz/pytask-io/6aadc53800502fbd596d9be15e8d3545a87df87a/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def event_loop(): 7 | 8 | loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() 9 | yield loop 10 | loop.close() 11 | -------------------------------------------------------------------------------- /tests/mock_scenario_one.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simulate how a client will interface with this library 3 | """ 4 | 5 | -------------------------------------------------------------------------------- /tests/mock_uow.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock Unit Of Work Functions 3 | """ 4 | import time 5 | 6 | 7 | def send_email(arg1: str): return arg1 8 | 9 | """ 10 | 11 | def send_email(arg1, arg2): time.sleep(1); return [arg1, arg2] 12 | 13 | pytask.add_unit_of_work(send_email, "Hello", 1) 14 | """ 15 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joegasewicz/pytask-io/6aadc53800502fbd596d9be15e8d3545a87df87a/tests/test_client.py -------------------------------------------------------------------------------- /tests/test_pytask_io.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | import time 4 | 5 | import pytest 6 | import redis 7 | from freezegun import freeze_time 8 | from pytask_io.exceptions import NotReadyException 9 | 10 | from pytask_io import PyTaskIO 11 | from pytask_io.client import client 12 | from pytask_io.utils import ( 13 | deserialize_store_data_sync, 14 | serialize_store_data, 15 | serialize_unit_of_work, 16 | ) 17 | from tests.mock_uow import send_email 18 | import dill 19 | 20 | 21 | class TestPyTaskIO: 22 | def setup_method(self): 23 | """Set up redis client and PyTaskIO""" 24 | self.r = redis.Redis(host="localhost", port=6379, db=0) 25 | 26 | def teardown_method(self): 27 | """Flush all from the store and close event loop""" 28 | self.r.flushall() 29 | loop = asyncio.get_event_loop() 30 | if loop.is_running(): 31 | loop.close() 32 | 33 | def test_init(self): 34 | """Test keywords assignment to attributes in __init__""" 35 | pytask = PyTaskIO( 36 | store_host="localhost", store_port=6379, store_db=0, workers=1 37 | ) 38 | assert pytask.store_host == "localhost" 39 | assert pytask.store_port == 6379 40 | assert pytask.store_db == 0 41 | assert pytask.workers == 1 42 | 43 | assert pytask.queue_client is None 44 | assert pytask.queue_store is None 45 | assert pytask.loop_thread is None 46 | assert pytask.main_loop is None 47 | 48 | def test_init_defaults_fallback(self): 49 | """Ensure PyTaskIO fallbacks to defaults if no options are passed.""" 50 | pytask = PyTaskIO() 51 | 52 | assert pytask.store_host == PyTaskIO.store_host 53 | assert pytask.store_port == PyTaskIO.store_port 54 | assert pytask.store_db == PyTaskIO.store_db 55 | assert pytask.workers == PyTaskIO.workers 56 | 57 | def test_run(self): 58 | """Ensure a thread is spawned and connection to host is made""" 59 | pytask = PyTaskIO( 60 | store_host="localhost", store_port=6379, store_db=0, workers=1 61 | ) 62 | pytask.run() 63 | 64 | assert len(threading.enumerate()) == 2 65 | 66 | new_thread = threading.enumerate()[1] 67 | 68 | assert new_thread.is_alive() is True 69 | assert new_thread.daemon is True 70 | assert new_thread.name == "event_loop" 71 | pytask.stop() 72 | 73 | @freeze_time("1955-11-12 12:00:00") 74 | def test_add_task(self): 75 | """Ensure a task is correctly added and result can be fetched for queue storage.""" 76 | pytask = PyTaskIO( 77 | store_host="localhost", store_port=6379, store_db=0, workers=1 78 | ) 79 | pytask.run() 80 | 81 | result = pytask.add_task(send_email, "Hello") 82 | assert result["store_type"] == "redis" 83 | assert result["store_name"] == "uow_result_#1" 84 | assert result["store_index"] == "1" 85 | assert result["store_db"] == 0 86 | assert result["store_created"] == "12/11/55 12:00:00" 87 | assert result["store_updated"] == "" 88 | assert result["queue_type"] == "redis" 89 | assert result["queue_name"] == "pytaskio_queue" 90 | assert result["queue_length"] == 2 91 | assert result["queue_db"] == 0 92 | assert result["queue_created"] == "12/11/55 12:00:00" 93 | assert result["queue_updated"] == "" 94 | assert result["unit_of_work"] == {} 95 | assert result["serialized_uow"] == b"\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x0etests.mock_uow\x94\x8c\nsend_email\x94\x93\x94]\x94\x8c\x05Hello\x94a\x86\x94." 96 | assert result["serialized_result"] == "" 97 | assert result["result_exec_date"] == "" 98 | pytask.stop() 99 | 100 | def test_poll_for_task(self): 101 | data = {"value_1": 1, "values_2": "hello"} 102 | 103 | dumped_data = serialize_store_data(data) 104 | self.r.lpush("task_result", dumped_data) 105 | 106 | expected = { 107 | "data": data, 108 | "list_name": "task_result", 109 | "task_index": 0, 110 | } 111 | 112 | task_meta = { 113 | "list_name": "task_result", 114 | "task_index": 0, 115 | } 116 | pytask = PyTaskIO( 117 | store_host="localhost", store_port=6379, store_db=0, workers=1 118 | ) 119 | pytask.run() 120 | assert pytask.poll_for_task(task_meta, tries=1, interval=1) == expected 121 | pytask.stop() 122 | 123 | def test_get_task_serialized_uow(self): 124 | pytask = PyTaskIO( 125 | store_host="localhost", store_port=6379, store_db=0, workers=1 126 | ) 127 | pytask.run() 128 | def send_email_quick(msg): 129 | return msg 130 | 131 | metadata = pytask.add_task(send_email_quick, "Hello Joe 1") 132 | time.sleep(1) 133 | 134 | assert metadata is not None 135 | result = pytask.get_task(metadata) 136 | serialized_fn = dill.dumps((send_email_quick, ["Hello Joe 1"])) 137 | assert result["serialized_uow"] == serialized_fn 138 | pytask.stop() 139 | 140 | def test_add_unit_of_work(self): 141 | 142 | # meta = pytask.add_task(send_email, "Hello world!") 143 | # TODO: Fix this, `brpop` is blocking as key `tasks` is not present. 144 | # assert serialize_unit_of_work(send_email, "Hello world!") in self.r.brpop("tasks") 145 | 146 | # Test if the uow is on the queue 147 | # Test if the uow is on the store 148 | 149 | # assert asyncio.get_running_loop() == True 150 | pass 151 | -------------------------------------------------------------------------------- /tests/test_task_queue.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import dill 3 | 4 | from pytask_io.utils import serialize_unit_of_work 5 | from tests.mock_uow import send_email 6 | 7 | 8 | def test_serialize_unit_of_work(): 9 | result = serialize_unit_of_work(send_email, "Hello", 1) 10 | assert isinstance(result, bytes) 11 | assert (send_email, ["Hello", 1]) == dill.loads(result) 12 | -------------------------------------------------------------------------------- /tests/test_worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | import redis 4 | 5 | from pytask_io.utils import serialize_unit_of_work 6 | from tests.mock_uow import send_email 7 | from pytask_io.worker import worker 8 | 9 | r = redis.Redis( 10 | host='localhost', 11 | port=6379, 12 | db=0, 13 | ) 14 | 15 | 16 | @pytest.mark.skip 17 | def test_worker(event_loop): 18 | dumped_uow = serialize_unit_of_work(send_email, ["Hello", 1]) 19 | r.lpush("tasks", dumped_uow) 20 | r.lpush("tasks", dumped_uow) 21 | r.lpush("tasks", dumped_uow) 22 | 23 | queue = asyncio.Queue() 24 | 25 | assert {} == event_loop.run_until_complete(worker(queue, r)) 26 | --------------------------------------------------------------------------------