From 94b24572ebd71915bdeab00fd98f141372e541ec Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Wed, 19 Oct 2022 03:10:04 +0300 Subject: [PATCH 01/15] added bot commands regarding team info --- poetry.lock | 325 ++++++++++++++++++++++++------------ pyproject.toml | 2 + requestor/bot/bot.py | 28 ++++ requestor/bot/create_bot.py | 10 ++ requestor/bot/handlers.py | 164 ++++++++++++++++++ requestor/models.py | 2 + requestor/settings.py | 7 + 7 files changed, 434 insertions(+), 104 deletions(-) create mode 100644 requestor/bot/bot.py create mode 100644 requestor/bot/create_bot.py create mode 100644 requestor/bot/handlers.py diff --git a/poetry.lock b/poetry.lock index aa82b30..b723f8d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,20 @@ +[[package]] +name = "aiogram" +version = "2.22.2" +description = "Is a pretty simple and fully asynchronous framework for Telegram Bot API" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +aiohttp = ">=3.8.0,<3.9.0" +Babel = ">=2.9.1,<2.10.0" +certifi = ">=2021.10.8" + +[package.extras] +fast = ["uvloop (>=0.16.0,<0.17.0)", "ujson (>=1.35)"] +proxy = ["aiohttp-socks (>=0.5.3,<0.6.0)"] + [[package]] name = "aiohttp" version = "3.8.3" @@ -113,6 +130,17 @@ python-versions = "*" pycodestyle = ">=2.8.0" toml = "*" +[[package]] +name = "babel" +version = "2.9.1" +description = "Internationalization utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + [[package]] name = "bandit" version = "1.7.4" @@ -154,6 +182,14 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2022.9.24" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "charset-normalizer" version = "2.1.1" @@ -260,7 +296,7 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.28" +version = "3.1.29" description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false @@ -271,7 +307,7 @@ gitdb = ">=4.0.1,<5" [[package]] name = "greenlet" -version = "1.1.3" +version = "1.1.3.post0" description = "Lightweight in-process concurrent programming" category = "main" optional = false @@ -645,6 +681,14 @@ python-versions = ">=3.6" [package.dependencies] pytest = ">=7.0" +[[package]] +name = "pytz" +version = "2022.5" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "6.0" @@ -684,7 +728,7 @@ python-versions = ">=3.6" [[package]] name = "sqlalchemy" -version = "1.4.41" +version = "1.4.42" description = "Database Abstraction Library" category = "main" optional = false @@ -716,7 +760,7 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "stevedore" -version = "4.0.0" +version = "4.0.1" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false @@ -749,6 +793,19 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "uvloop" +version = "0.17.0" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +dev = ["Cython (>=0.29.32,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=22.0.0,<22.1.0)", "mypy (>=0.800)", "aiohttp"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=22.0.0,<22.1.0)", "mypy (>=0.800)", "Cython (>=0.29.32,<0.30.0)", "aiohttp"] + [[package]] name = "werkzeug" version = "2.2.2" @@ -798,9 +855,13 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "4e82488f1e8f40b6a4df307656a0ea385366acdef41fbc9b05b4d65a14f5f29c" +content-hash = "72b3c6e9d107fc3cf206e4d2bf7b89e953730658bbd1ff94f32f9847d7a9a11f" [metadata.files] +aiogram = [ + {file = "aiogram-2.22.2-py3-none-any.whl", hash = "sha256:d2c5068cc89fc4012038268e111ca9249f66decce817522ec4d215dd829bc507"}, + {file = "aiogram-2.22.2.tar.gz", hash = "sha256:02e4120122af423575d48519cb469d15189f5da00a3237e715d492fe6d798bfd"}, +] aiohttp = [ {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, @@ -934,6 +995,10 @@ autopep8 = [ {file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"}, {file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"}, ] +babel = [ + {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, + {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, +] bandit = [ {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, @@ -963,6 +1028,10 @@ black = [ {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] charset-normalizer = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, @@ -1099,64 +1168,76 @@ gitdb = [ {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, ] gitpython = [ - {file = "GitPython-3.1.28-py3-none-any.whl", hash = "sha256:77bfbd299d8709f6af7e0c70840ef26e7aff7cf0c1ed53b42dd7fc3a310fcb02"}, - {file = "GitPython-3.1.28.tar.gz", hash = "sha256:6bd3451b8271132f099ceeaf581392eaf6c274af74bb06144307870479d0697c"}, + {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"}, + {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"}, ] greenlet = [ - {file = "greenlet-1.1.3-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b"}, - {file = "greenlet-1.1.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32"}, - {file = "greenlet-1.1.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a"}, - {file = "greenlet-1.1.3-cp27-cp27m-win32.whl", hash = "sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318"}, - {file = "greenlet-1.1.3-cp27-cp27m-win_amd64.whl", hash = "sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49"}, - {file = "greenlet-1.1.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2"}, - {file = "greenlet-1.1.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e"}, - {file = "greenlet-1.1.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8"}, - {file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477"}, - {file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700"}, - {file = "greenlet-1.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26"}, - {file = "greenlet-1.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96"}, - {file = "greenlet-1.1.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7"}, - {file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47"}, - {file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2"}, - {file = "greenlet-1.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5"}, - {file = "greenlet-1.1.3-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa"}, - {file = "greenlet-1.1.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4"}, - {file = "greenlet-1.1.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76"}, - {file = "greenlet-1.1.3-cp35-cp35m-win32.whl", hash = "sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c"}, - {file = "greenlet-1.1.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20"}, - {file = "greenlet-1.1.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910"}, - {file = "greenlet-1.1.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d"}, - {file = "greenlet-1.1.3-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33"}, - {file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57"}, - {file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90"}, - {file = "greenlet-1.1.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e"}, - {file = "greenlet-1.1.3-cp36-cp36m-win32.whl", hash = "sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79"}, - {file = "greenlet-1.1.3-cp36-cp36m-win_amd64.whl", hash = "sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0"}, - {file = "greenlet-1.1.3-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3"}, - {file = "greenlet-1.1.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e"}, - {file = "greenlet-1.1.3-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba"}, - {file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080"}, - {file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41"}, - {file = "greenlet-1.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268"}, - {file = "greenlet-1.1.3-cp37-cp37m-win32.whl", hash = "sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc"}, - {file = "greenlet-1.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed"}, - {file = "greenlet-1.1.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08"}, - {file = "greenlet-1.1.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e"}, - {file = "greenlet-1.1.3-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25"}, - {file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380"}, - {file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809"}, - {file = "greenlet-1.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd"}, - {file = "greenlet-1.1.3-cp38-cp38-win32.whl", hash = "sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9"}, - {file = "greenlet-1.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202"}, - {file = "greenlet-1.1.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2"}, - {file = "greenlet-1.1.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe"}, - {file = "greenlet-1.1.3-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905"}, - {file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382"}, - {file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600"}, - {file = "greenlet-1.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403"}, - {file = "greenlet-1.1.3-cp39-cp39-win32.whl", hash = "sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1"}, - {file = "greenlet-1.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932"}, - {file = "greenlet-1.1.3.tar.gz", hash = "sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-win32.whl", hash = "sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-win_amd64.whl", hash = "sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519"}, + {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392"}, + {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-win_amd64.whl", hash = "sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-win32.whl", hash = "sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-win32.whl", hash = "sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-win32.whl", hash = "sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-win32.whl", hash = "sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-win_amd64.whl", hash = "sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-win32.whl", hash = "sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-win_amd64.whl", hash = "sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7"}, + {file = "greenlet-1.1.3.post0.tar.gz", hash = "sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1565,6 +1646,10 @@ pytest-subtests = [ {file = "pytest-subtests-0.8.0.tar.gz", hash = "sha256:46eb376022e926950816ccc23502de3277adcc1396652ddb3328ce0289052c4d"}, {file = "pytest_subtests-0.8.0-py3-none-any.whl", hash = "sha256:4e28ca52cf7a46645c1ded7933745b69334cdc97a412ed4431f7be7cef9a0994"}, ] +pytz = [ + {file = "pytz-2022.5-py2.py3-none-any.whl", hash = "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22"}, + {file = "pytz-2022.5.tar.gz", hash = "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914"}, +] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, @@ -1613,51 +1698,51 @@ smmap = [ {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.41-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d"}, - {file = "SQLAlchemy-1.4.41-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330"}, - {file = "SQLAlchemy-1.4.41-cp27-cp27m-win32.whl", hash = "sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d"}, - {file = "SQLAlchemy-1.4.41-cp27-cp27m-win_amd64.whl", hash = "sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05"}, - {file = "SQLAlchemy-1.4.41-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc"}, - {file = "SQLAlchemy-1.4.41-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a"}, - {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc"}, - {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad"}, - {file = "SQLAlchemy-1.4.41-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e"}, - {file = "SQLAlchemy-1.4.41-cp310-cp310-win32.whl", hash = "sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd"}, - {file = "SQLAlchemy-1.4.41-cp310-cp310-win_amd64.whl", hash = "sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251"}, - {file = "SQLAlchemy-1.4.41-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536"}, - {file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb"}, - {file = "SQLAlchemy-1.4.41-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded"}, - {file = "SQLAlchemy-1.4.41-cp311-cp311-win32.whl", hash = "sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0"}, - {file = "SQLAlchemy-1.4.41-cp311-cp311-win_amd64.whl", hash = "sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1"}, - {file = "SQLAlchemy-1.4.41-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546"}, - {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b"}, - {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b"}, - {file = "SQLAlchemy-1.4.41-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c"}, - {file = "SQLAlchemy-1.4.41-cp36-cp36m-win32.whl", hash = "sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682"}, - {file = "SQLAlchemy-1.4.41-cp36-cp36m-win_amd64.whl", hash = "sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892"}, - {file = "SQLAlchemy-1.4.41-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26"}, - {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8"}, - {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9"}, - {file = "SQLAlchemy-1.4.41-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497"}, - {file = "SQLAlchemy-1.4.41-cp37-cp37m-win32.whl", hash = "sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767"}, - {file = "SQLAlchemy-1.4.41-cp37-cp37m-win_amd64.whl", hash = "sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d"}, - {file = "SQLAlchemy-1.4.41-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33"}, - {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0"}, - {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c"}, - {file = "SQLAlchemy-1.4.41-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf"}, - {file = "SQLAlchemy-1.4.41-cp38-cp38-win32.whl", hash = "sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a"}, - {file = "SQLAlchemy-1.4.41-cp38-cp38-win_amd64.whl", hash = "sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288"}, - {file = "SQLAlchemy-1.4.41-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c"}, - {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab"}, - {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd"}, - {file = "SQLAlchemy-1.4.41-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c"}, - {file = "SQLAlchemy-1.4.41-cp39-cp39-win32.whl", hash = "sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d"}, - {file = "SQLAlchemy-1.4.41-cp39-cp39-win_amd64.whl", hash = "sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"}, - {file = "SQLAlchemy-1.4.41.tar.gz", hash = "sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-win32.whl", hash = "sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-win_amd64.whl", hash = "sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-win32.whl", hash = "sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-win_amd64.whl", hash = "sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-win32.whl", hash = "sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-win_amd64.whl", hash = "sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-win32.whl", hash = "sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-win_amd64.whl", hash = "sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-win32.whl", hash = "sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-win_amd64.whl", hash = "sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-win32.whl", hash = "sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-win_amd64.whl", hash = "sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-win32.whl", hash = "sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-win_amd64.whl", hash = "sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934"}, + {file = "SQLAlchemy-1.4.42.tar.gz", hash = "sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363"}, ] stevedore = [ - {file = "stevedore-4.0.0-py3-none-any.whl", hash = "sha256:87e4d27fe96d0d7e4fc24f0cbe3463baae4ec51e81d95fbe60d2474636e0c7d8"}, - {file = "stevedore-4.0.0.tar.gz", hash = "sha256:f82cc99a1ff552310d19c379827c2c64dd9f85a38bcd5559db2470161867b786"}, + {file = "stevedore-4.0.1-py3-none-any.whl", hash = "sha256:01645addb67beff04c7cfcbb0a6af8327d2efc3380b0f034aa316d4576c4d470"}, + {file = "stevedore-4.0.1.tar.gz", hash = "sha256:9a23111a6e612270c591fd31ff3321c6b5f3d5f3dabb1427317a5ab608fc261a"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1671,6 +1756,38 @@ typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] +uvloop = [ + {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, + {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, + {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, + {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, + {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, + {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, + {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, + {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, + {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, + {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, + {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, + {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, + {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, + {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, + {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, + {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, + {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, + {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, + {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, + {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, + {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, + {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, + {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, + {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, + {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, + {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, + {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, + {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, + {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, + {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, +] werkzeug = [ {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, diff --git a/pyproject.toml b/pyproject.toml index b3a4afe..3af65b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ orjson = "^3.5.3" alembic = "^1.6.5" aiohttp = "^3.7.4" asyncpg = "^0.23.0" +aiogram = "^2.22.2" +uvloop = "^0.17.0" [tool.poetry.dev-dependencies] black = "22.3.0" diff --git a/requestor/bot/bot.py b/requestor/bot/bot.py new file mode 100644 index 0000000..a58e51d --- /dev/null +++ b/requestor/bot/bot.py @@ -0,0 +1,28 @@ +import asyncio + +from asyncpg import create_pool + +from requestor.db import DBService +from requestor.settings import get_config + +from .create_bot import dp +from .handlers import register_handlers + + +async def main(): + config = get_config() + db_config = config.db_config.dict() + pool_config = db_config.pop("db_pool_config") + pool_config["dsn"] = pool_config.pop("db_url") + pool = create_pool(**pool_config) + db_service = DBService(pool=pool) + try: + register_handlers(dp, db_service, config) + await db_service.setup() + await dp.start_polling() + finally: + await db_service.cleanup() + + +if __name__ == "__main__": + asyncio.run(main()) # type: ignore diff --git a/requestor/bot/create_bot.py b/requestor/bot/create_bot.py new file mode 100644 index 0000000..58d46d9 --- /dev/null +++ b/requestor/bot/create_bot.py @@ -0,0 +1,10 @@ +from aiogram import Bot +from aiogram.dispatcher import Dispatcher + +from requestor.settings import get_config + +config = get_config() + +bot = Bot(token=config.telegram_config.bot_token) + +dp = Dispatcher(bot) diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py new file mode 100644 index 0000000..4be532f --- /dev/null +++ b/requestor/bot/handlers.py @@ -0,0 +1,164 @@ +import traceback +import typing as tp +from functools import partial + +from aiogram import Dispatcher, types + +from requestor.db import DBService, DuplicatedTeamError, TeamNotFoundError +from requestor.log import app_logger +from requestor.models import TeamInfo +from requestor.settings import ServiceConfig + + +def parse_msg_with_team_info(message: types.Message) -> tp.Optional[TeamInfo]: + command = message.text.split(maxsplit=3) + if len(command) == 4: + _, title, api_base_url, api_key = command + elif len(command) == 3: + _, title, api_base_url = command + api_key = None + + try: + return TeamInfo( + title=title, chat_id=message.chat.id, api_base_url=api_base_url, api_key=api_key + ) + except NameError: + return None + + +async def handle(handler, db_service: DBService, message: types.Message) -> None: + try: + await handler(message, db_service) + except Exception: + app_logger.error(traceback.format_exc()) + raise + + +async def start_h(message: types.Message, db_service: DBService) -> None: + reply = ( + "Привет! Я бот, который будет проверять сервисы " + "в рамках курса по рекомендательным системам. " + "Наберите /help для вывода списка доступных команд." + ) + await message.reply(reply) + + +async def help_h(event: types.Message, db_service: DBService) -> None: + reply = ( + "Список доступных команд:\n" + "/register_team team_name api_host api_key (опционально) - для регистрации команд\n" + "/update_team team_name api_host api_key (опционально) - для обновления данных команды\n" + "/show_current_team - для вывода данных по зарегистрированной команде\n" + ) + await event.reply(reply) + + +async def register_team_h(message: types.Message, db_service: DBService) -> None: + team_info = parse_msg_with_team_info(message) + + if team_info is None: + await message.reply( + "Пожалуйста, введите данные в корректном формате. " + "/register_team team_name api_host api_key (опционально)" + ) + return + + try: + await db_service.add_team(team_info) + await message.reply(f"Команда `{team_info.title}` успешно зарегистрирована!") + # TODO: somehow deduplicate code? wrapper? + except DuplicatedTeamError as e: + if e.column == "chat_id": + await message.reply( + "Вы уже регистрировали команду. Если необходимо обновить что-то, " + "пожалуйста, воспользуйтесь командой /update_team." + ) + elif e.column == "title": + await message.reply( + f"Команда с именем `{team_info.title}` уже существует. " + "Пожалуйста, выберите другое имя команды." + ) + elif e.column == "api_base_url": + await message.reply( + f"Хост: `{team_info.api_base_url}` уже кем-то используется. " + "Пожалуйста, выберите другой хост." + ) + else: + await message.reply(e) + + +async def update_team_h(message: types.Message, db_service: DBService) -> None: + updated_team_info = parse_msg_with_team_info(message) + if updated_team_info is None: + await message.reply( + "Пожалуйста, введите данные в корректном формате. " + "/update_team team_name api_host api_key (опционально)" + ) + return + + current_team_info = await db_service.get_team_by_chat(message.chat.id) + + try: + await db_service.update_team(current_team_info.team_id, updated_team_info) + await message.reply( + "Данные по вашей команде были обновлены. Воспользуйтесь командой /show_current_team" + ) + except TeamNotFoundError: + await message.reply( + "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." + ) + # TODO: somehow deduplicate code? wrapper? + except DuplicatedTeamError as e: + if e.column == "chat_id": + await message.reply( + "Вы уже регистрировали команду. Если необходимо обновить что-то, " + "пожалуйста, воспользуйтесь командой /update_team." + ) + elif e.column == "title": + await message.reply( + f"Команда с именем `{updated_team_info.title}` уже существует. " + "Пожалуйста, выберите другое имя команды." + ) + elif e.column == "api_base_url": + await message.reply( + f"Хост: `{updated_team_info.api_base_url}` уже кем-то используется. " + "Пожалуйста, выберите другой хост." + ) + else: + await message.reply(e) + + +async def show_current_team_h(message: types.Message, db_service: DBService) -> None: + try: + team_info = await db_service.get_team_by_chat(message.chat.id) + await message.reply( + f"Команда: {team_info.title}\n" + f"Хост: {team_info.api_base_url}\n" + f"API Токен: {team_info.api_key if team_info.api_key is not None else 'Отсутствует'}\n" + ) + except TeamNotFoundError: + await message.reply( + "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." + ) + + +async def other_messages_h(message: types.Message, db_service: DBService) -> None: + await message.reply("Я не поддерживаю Inline команды. Пожалуйста, воспользуйтесь /help.") + + +def register_handlers(dp: Dispatcher, db_service: DBService, config: ServiceConfig) -> None: + bot_name = config.telegram_config.bot_name + command_handlers_mapping = { + "start": start_h, + "help": help_h, + "register_team": register_team_h, + "update_team": update_team_h, + "show_current_team": show_current_team_h, + } + + for command, handler in command_handlers_mapping.items(): + dp.register_message_handler(partial(handle, handler, db_service), commands=[command]) + + dp.register_message_handler( + partial(handle, other_messages_h, db_service), regexp=rf"@{bot_name}" + ) diff --git a/requestor/models.py b/requestor/models.py index 7402034..ae6d5de 100644 --- a/requestor/models.py +++ b/requestor/models.py @@ -10,6 +10,8 @@ class TeamInfo(BaseModel): title: str chat_id: int api_base_url: str + # TODO: move from python3.10 to 3.8 with poetry to fix issues with pylint + # https://github.com/PyCQA/pylint/issues/1498#issuecomment-717978456 api_key: tp.Optional[str] diff --git a/requestor/settings.py b/requestor/settings.py index 8181c5b..48f8a7d 100644 --- a/requestor/settings.py +++ b/requestor/settings.py @@ -33,13 +33,20 @@ class DBConfig(Config): db_pool_config: DBPoolConfig +class TelegramConfig(Config): + bot_token: str + bot_name: str + + class ServiceConfig(Config): log_config: LogConfig db_config: DBConfig + telegram_config: TelegramConfig def get_config() -> ServiceConfig: return ServiceConfig( log_config=LogConfig(), db_config=DBConfig(db_pool_config=DBPoolConfig()), + telegram_config=TelegramConfig(), ) From 111846847ebad52c5203bfbc52d88e9d8baae5e2 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Wed, 19 Oct 2022 03:19:33 +0300 Subject: [PATCH 02/15] fixed github ci --- .github/workflows/cicd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 849b6f6..765d407 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -6,6 +6,8 @@ jobs: runs-on: ubuntu-20.04 env: DB_URL: postgresql://user:pass@127.0.0.1:5432/db + BOT_TOKEN: BOT_TOKEN + BOT_NAME: BOT_NAME services: From 864d5f7206ca79bd766a554ff938cd9b5a4a2555 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Fri, 21 Oct 2022 00:20:08 +0300 Subject: [PATCH 03/15] temp state --- main.py | 18 +++++- requestor/bot/__init__.py | 9 +++ requestor/bot/bot.py | 28 ---------- requestor/bot/handlers.py | 114 +++++++++++++++++++++----------------- requestor/models.py | 2 - 5 files changed, 88 insertions(+), 83 deletions(-) delete mode 100644 requestor/bot/bot.py diff --git a/main.py b/main.py index 244a421..e4e052a 100644 --- a/main.py +++ b/main.py @@ -1,2 +1,18 @@ +import asyncio + +from requestor.bot import dp, config, register_handlers +from requestor.services import make_db_service + + +async def main(): + db_service = make_db_service(config) + register_handlers(dp, db_service, config) + await db_service.setup() + try: + await dp.start_polling() + finally: + await db_service.cleanup() + + if __name__ == "__main__": - pass + asyncio.run(main()) diff --git a/requestor/bot/__init__.py b/requestor/bot/__init__.py index e69de29..585a435 100644 --- a/requestor/bot/__init__.py +++ b/requestor/bot/__init__.py @@ -0,0 +1,9 @@ +from .create_bot import bot, config, dp +from .handlers import register_handlers + +__all__ = ( + "dp", + "bot", + "config", + "register_handlers", +) diff --git a/requestor/bot/bot.py b/requestor/bot/bot.py deleted file mode 100644 index a58e51d..0000000 --- a/requestor/bot/bot.py +++ /dev/null @@ -1,28 +0,0 @@ -import asyncio - -from asyncpg import create_pool - -from requestor.db import DBService -from requestor.settings import get_config - -from .create_bot import dp -from .handlers import register_handlers - - -async def main(): - config = get_config() - db_config = config.db_config.dict() - pool_config = db_config.pop("db_pool_config") - pool_config["dsn"] = pool_config.pop("db_url") - pool = create_pool(**pool_config) - db_service = DBService(pool=pool) - try: - register_handlers(dp, db_service, config) - await db_service.setup() - await dp.start_polling() - finally: - await db_service.cleanup() - - -if __name__ == "__main__": - asyncio.run(main()) # type: ignore diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py index 4be532f..f78b0be 100644 --- a/requestor/bot/handlers.py +++ b/requestor/bot/handlers.py @@ -3,6 +3,8 @@ from functools import partial from aiogram import Dispatcher, types +from aiogram.types import ParseMode +from aiogram.utils.markdown import bold, text from requestor.db import DBService, DuplicatedTeamError, TeamNotFoundError from requestor.log import app_logger @@ -35,112 +37,120 @@ async def handle(handler, db_service: DBService, message: types.Message) -> None async def start_h(message: types.Message, db_service: DBService) -> None: - reply = ( - "Привет! Я бот, который будет проверять сервисы " - "в рамках курса по рекомендательным системам. " - "Наберите /help для вывода списка доступных команд." + reply = text( + "Привет! Я бот, который будет проверять сервисы", + "в рамках курса по рекомендательным системам.", + "Наберите /help для вывода списка доступных команд.", ) await message.reply(reply) async def help_h(event: types.Message, db_service: DBService) -> None: - reply = ( - "Список доступных команд:\n" - "/register_team team_name api_host api_key (опционально) - для регистрации команд\n" - "/update_team team_name api_host api_key (опционально) - для обновления данных команды\n" - "/show_current_team - для вывода данных по зарегистрированной команде\n" + reply = text( + bold("Список доступных команд:"), + "/register_team team_name api_host api_key (опционально) - для регистрации команд", + "/update_team team_name api_host api_key (опционально) - для обновления данных команды", + "/show_current_team - для вывода данных по зарегистрированной команде", + sep="\n", ) - await event.reply(reply) + await event.reply(reply, parse_mode=ParseMode.MARKDOWN) async def register_team_h(message: types.Message, db_service: DBService) -> None: team_info = parse_msg_with_team_info(message) if team_info is None: - await message.reply( - "Пожалуйста, введите данные в корректном формате. " - "/register_team team_name api_host api_key (опционально)" + reply = text( + "Пожалуйста, введите данные в корректном формате.", + "/register\_team team\_name api\_host api\_key (опционально)", ) + await message.reply(reply) return try: await db_service.add_team(team_info) - await message.reply(f"Команда `{team_info.title}` успешно зарегистрирована!") - # TODO: somehow deduplicate code? wrapper? + reply = f"Команда `{team_info.title}` успешно зарегистрирована!" except DuplicatedTeamError as e: if e.column == "chat_id": - await message.reply( - "Вы уже регистрировали команду. Если необходимо обновить что-то, " - "пожалуйста, воспользуйтесь командой /update_team." + reply = text( + "Вы уже регистрировали команду. Если необходимо обновить что-то,", + "пожалуйста, воспользуйтесь командой /update_team.", ) elif e.column == "title": - await message.reply( - f"Команда с именем `{team_info.title}` уже существует. " - "Пожалуйста, выберите другое имя команды." + reply = text( + f"Команда с именем `{team_info.title}` уже существует.", + "Пожалуйста, выберите другое имя команды.", ) elif e.column == "api_base_url": - await message.reply( - f"Хост: `{team_info.api_base_url}` уже кем-то используется. " - "Пожалуйста, выберите другой хост." + reply = text( + f"Хост: `{team_info.api_base_url}` уже кем-то используется.", + "Пожалуйста, выберите другой хост.", ) else: - await message.reply(e) + reply = text( + "Что-то пошло не так.", + "Пожалуйста, попробуйте зарегистрироваться через несколько минут.", + ) + + await message.reply(reply) async def update_team_h(message: types.Message, db_service: DBService) -> None: updated_team_info = parse_msg_with_team_info(message) + if updated_team_info is None: - await message.reply( - "Пожалуйста, введите данные в корректном формате. " - "/update_team team_name api_host api_key (опционально)" + reply = text( + "Пожалуйста, введите данные в корректном формате.", + "/update_team team_name api_host api_key (опционально)", ) + await message.reply(reply) return current_team_info = await db_service.get_team_by_chat(message.chat.id) try: await db_service.update_team(current_team_info.team_id, updated_team_info) - await message.reply( - "Данные по вашей команде были обновлены. Воспользуйтесь командой /show_current_team" + reply = ( + "Данные по вашей команде были обновлены. Воспользуйтесь командой /show_current_team." ) except TeamNotFoundError: - await message.reply( - "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." - ) - # TODO: somehow deduplicate code? wrapper? + reply = "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." except DuplicatedTeamError as e: - if e.column == "chat_id": - await message.reply( - "Вы уже регистрировали команду. Если необходимо обновить что-то, " - "пожалуйста, воспользуйтесь командой /update_team." - ) - elif e.column == "title": - await message.reply( - f"Команда с именем `{updated_team_info.title}` уже существует. " - "Пожалуйста, выберите другое имя команды." + if e.column == "title": + reply = text( + f"Команда с именем `{updated_team_info.title}` уже существует.", + "Пожалуйста, выберите другое имя команды.", ) elif e.column == "api_base_url": - await message.reply( - f"Хост: `{updated_team_info.api_base_url}` уже кем-то используется. " - "Пожалуйста, выберите другой хост." + reply = text( + f"Хост: `{updated_team_info.api_base_url}` уже кем-то используется.", + "Пожалуйста, выберите другой хост.", ) else: - await message.reply(e) + reply = text( + "Что-то пошло не так.", + "Пожалуйста, попробуйте зарегистрироваться через несколько минут.", + ) + await message.reply(reply) async def show_current_team_h(message: types.Message, db_service: DBService) -> None: try: team_info = await db_service.get_team_by_chat(message.chat.id) - await message.reply( - f"Команда: {team_info.title}\n" - f"Хост: {team_info.api_base_url}\n" - f"API Токен: {team_info.api_key if team_info.api_key is not None else 'Отсутствует'}\n" + api_key = team_info.api_key if team_info.api_key is not None else "Отсутствует" + reply = text( + f"{bold('Команда')}: {team_info.title}", + f"{bold('Хост')}: {team_info.api_base_url}", + f"{bold('API Токен')}: {api_key}", + sep="\n", ) except TeamNotFoundError: - await message.reply( + reply = text( "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." ) + await message.reply(reply, parse_mode=ParseMode.MARKDOWN) + async def other_messages_h(message: types.Message, db_service: DBService) -> None: await message.reply("Я не поддерживаю Inline команды. Пожалуйста, воспользуйтесь /help.") diff --git a/requestor/models.py b/requestor/models.py index ae6d5de..7402034 100644 --- a/requestor/models.py +++ b/requestor/models.py @@ -10,8 +10,6 @@ class TeamInfo(BaseModel): title: str chat_id: int api_base_url: str - # TODO: move from python3.10 to 3.8 with poetry to fix issues with pylint - # https://github.com/PyCQA/pylint/issues/1498#issuecomment-717978456 api_key: tp.Optional[str] From 81194caee633167c429ffd017a38b695525cabd5 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 05:01:03 +0300 Subject: [PATCH 04/15] added all handlers --- main.py | 7 +- requestor/bot/__init__.py | 2 + requestor/bot/bot_utils.py | 42 +++++++++ requestor/bot/commands.py | 121 +++++++++++++++++++++++++ requestor/bot/constants.py | 15 ++++ requestor/bot/handlers.py | 177 +++++++++++++++++++++++-------------- requestor/db/__init__.py | 2 + requestor/log.py | 3 +- 8 files changed, 300 insertions(+), 69 deletions(-) create mode 100644 requestor/bot/bot_utils.py create mode 100644 requestor/bot/commands.py create mode 100644 requestor/bot/constants.py diff --git a/main.py b/main.py index e4e052a..94ff0ba 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,15 @@ import asyncio -from requestor.bot import dp, config, register_handlers +from requestor.bot import dp, config, register_handlers, bot, BotCommands from requestor.services import make_db_service - +from requestor.log import setup_logging, app_logger async def main(): db_service = make_db_service(config) register_handlers(dp, db_service, config) + setup_logging(config) + + await bot.set_my_commands(commands=BotCommands.get_bot_commands()) await db_service.setup() try: await dp.start_polling() diff --git a/requestor/bot/__init__.py b/requestor/bot/__init__.py index 585a435..5991a1a 100644 --- a/requestor/bot/__init__.py +++ b/requestor/bot/__init__.py @@ -1,3 +1,4 @@ +from .commands import BotCommands from .create_bot import bot, config, dp from .handlers import register_handlers @@ -6,4 +7,5 @@ "bot", "config", "register_handlers", + "BotCommands", ) diff --git a/requestor/bot/bot_utils.py b/requestor/bot/bot_utils.py new file mode 100644 index 0000000..1cf0a5c --- /dev/null +++ b/requestor/bot/bot_utils.py @@ -0,0 +1,42 @@ +import typing as tp + +from aiogram import types + +from requestor.models import TeamInfo + + +# TODO: somehow try generalize this func to reduce duplicate code +def parse_msg_with_team_info( + message: types.Message, +) -> tp.Tuple[tp.Optional[str], tp.Optional[TeamInfo]]: + args = message.get_args().split() + n_args = len(args) + if n_args == 4: + token, title, api_base_url, api_key = args + elif n_args == 3: + token, title, api_base_url = args + api_key = None + + try: + return token, TeamInfo( + title=title, chat_id=message.chat.id, api_base_url=api_base_url, api_key=api_key + ) + except NameError: + return None, None + + +def parse_msg_with_model_info( + message: types.Message, +) -> tp.Tuple[tp.Optional[str], tp.Optional[str]]: + args = message.get_args().split(maxsplit=1) + n_args = len(args) + if n_args == 2: + name, description = args + elif n_args == 1: + name = args[0] + description = None + + try: + return name, description + except NameError: + return None, None diff --git a/requestor/bot/commands.py b/requestor/bot/commands.py new file mode 100644 index 0000000..a9da9df --- /dev/null +++ b/requestor/bot/commands.py @@ -0,0 +1,121 @@ +import typing as tp +from dataclasses import dataclass +from enum import Enum + +from aiogram.types import BotCommand +from aiogram.utils.markdown import text + + +@dataclass +class CommandDescription: + command_name: str + short_description: str + long_description: tp.Optional[str] = None + + +commands_description = ( + ( + "start", + "Начало работы с ботом", + ), + ( + "help", + "Список доступных команд", + ), + ( + "register_team", + "Регистрация команды", + text( + "С помощью этой команды можно зарегистрировать свою команду", + "Принимает на вход аргументы через пробел:", + "token - токен, который генерируется индивидуально для каждой команды.", + "title - название команды, без пробелов.", + "api_base_url - хост, по которому будет находиться API команды.", + "api_key - опционально, токен для запрашивания API.", + "Пример использования:", + "/register_team 123 MyTeamName http://myapi.ru/api/v1 MyApiKey", + sep="\n", + ), + ), + ( + "update_team", + "Обновление информации команды", + text( + "С помощью этой команды можно обновить хост или токен API.", + "Для этого используются соответствующие аргументы через пробел.", + "api_base_url - хост, по которому будет находиться API команды.", + "api_key - токен для запрашивания API.", + "Пример использования для обновления хоста:", + "/update_team api_base_url http://myapi.ru/api/v2", + sep="\n", + ), + ), + ( + "show_current_team", + "Вывод информацию по текущей команде", + "Выводит название команды, хоста и API токена", + ), + ( + "add_model", + "Добавление новой модели", + text( + "С помощью этой команды можно добавить модели для проверки.", + "Для этого используются следующие аргументы:", + "name - хост, по которому будет находиться API команды.", + "description - опционально, более подробное описание модели", + "Пример использования для обновления хоста:", + "/add_model lightfm_64", + ("Далее модели будут запрашиваться по адресу: " "{api_base_url}/{name}/{user_id}"), + ( + "То есть адрес для запроса выглядит, например, так: " + "http://myapi.ru/api/v1/lightfm_64/123" + ), + sep="\n", + ), + ), + ( + "show_models", + "Вывод информации по добавленным моделям", + text( + "С помощью этой команды можно вывести следующую информацию:", + "Название, описание (если присутствует) и дату добавления модели." + "Если было добавлено более 10 моделей, то выведутся последние 10 по дате добавления", + sep="\n", + ), + ), + # TODO: create request command + ("request", "Запрос рекомендаций по модели", "Какое-то описание"), +) + +cmd2cls_desc = {args[0]: CommandDescription(*args) for args in commands_description} + + +# it can be initialized via Enum("BotCommands", cmd2cls_desc) +# but IDE doesn't provide you with helper annotations +# and you can not add class methods without "hacks" +# TODO: think of simple way instantiate a frozen class +# with typehinting from IDE +class BotCommands(Enum): + start: CommandDescription = cmd2cls_desc["start"] + help: CommandDescription = cmd2cls_desc["help"] + register_team: CommandDescription = cmd2cls_desc["register_team"] + update_team: CommandDescription = cmd2cls_desc["update_team"] + show_current_team: CommandDescription = cmd2cls_desc["show_current_team"] + add_model: CommandDescription = cmd2cls_desc["add_model"] + show_models: CommandDescription = cmd2cls_desc["show_models"] + + @classmethod + def get_bot_commands(cls) -> tp.List[BotCommand]: + return [ + BotCommand(command=command.name, description=command.value.short_description) + for command in BotCommands + ] + + @classmethod + def get_description_for_available_commands(cls) -> str: + descriptions = [] + for command in BotCommands: + if command not in (BotCommands.start, BotCommands.help): + descriptions.append(f"/{command.name}\n{command.value.long_description}") + + return "\n\n".join(descriptions) diff --git a/requestor/bot/constants.py b/requestor/bot/constants.py new file mode 100644 index 0000000..7d31910 --- /dev/null +++ b/requestor/bot/constants.py @@ -0,0 +1,15 @@ +import typing as tp + +AVAILABLE_FOR_UPDATE: tp.Final = { + "api_base_url", + "api_key", +} + + +INCORRECT_DATA_IN_MSG: tp.Final = ( + "Пожалуйста, введите данные в корректном формате. " "Используйте команду /help для справки." +) + +TEAM_NOT_FOUND_MSG: tp.Final = ( + "Команда от вашего чата не найдена. Скорее всего, вы еще не регистрировались." +) diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py index f78b0be..978f99a 100644 --- a/requestor/bot/handlers.py +++ b/requestor/bot/handlers.py @@ -1,31 +1,24 @@ import traceback -import typing as tp from functools import partial from aiogram import Dispatcher, types from aiogram.types import ParseMode -from aiogram.utils.markdown import bold, text - -from requestor.db import DBService, DuplicatedTeamError, TeamNotFoundError +from aiogram.utils.markdown import bold, escape_md, text + +from requestor.db import ( + DBService, + DuplicatedModelError, + DuplicatedTeamError, + TeamNotFoundError, + TokenNotFoundError, +) from requestor.log import app_logger -from requestor.models import TeamInfo +from requestor.models import ModelInfo, TeamInfo from requestor.settings import ServiceConfig - -def parse_msg_with_team_info(message: types.Message) -> tp.Optional[TeamInfo]: - command = message.text.split(maxsplit=3) - if len(command) == 4: - _, title, api_base_url, api_key = command - elif len(command) == 3: - _, title, api_base_url = command - api_key = None - - try: - return TeamInfo( - title=title, chat_id=message.chat.id, api_base_url=api_base_url, api_key=api_key - ) - except NameError: - return None +from .bot_utils import parse_msg_with_model_info, parse_msg_with_team_info +from .commands import BotCommands +from .constants import AVAILABLE_FOR_UPDATE, INCORRECT_DATA_IN_MSG, TEAM_NOT_FOUND_MSG async def handle(handler, db_service: DBService, message: types.Message) -> None: @@ -46,30 +39,25 @@ async def start_h(message: types.Message, db_service: DBService) -> None: async def help_h(event: types.Message, db_service: DBService) -> None: - reply = text( - bold("Список доступных команд:"), - "/register_team team_name api_host api_key (опционально) - для регистрации команд", - "/update_team team_name api_host api_key (опционально) - для обновления данных команды", - "/show_current_team - для вывода данных по зарегистрированной команде", - sep="\n", - ) - await event.reply(reply, parse_mode=ParseMode.MARKDOWN) + reply = BotCommands.get_description_for_available_commands() + await event.reply(reply) async def register_team_h(message: types.Message, db_service: DBService) -> None: - team_info = parse_msg_with_team_info(message) + token, team_info = parse_msg_with_team_info(message) if team_info is None: - reply = text( - "Пожалуйста, введите данные в корректном формате.", - "/register\_team team\_name api\_host api\_key (опционально)", - ) - await message.reply(reply) - return + return await message.reply(INCORRECT_DATA_IN_MSG) try: - await db_service.add_team(team_info) + await db_service.add_team(team_info, token) reply = f"Команда `{team_info.title}` успешно зарегистрирована!" + except TokenNotFoundError: + reply = text( + "Токен не найден. Пожалуйста, проверьте написание.", + f"Точно ли токен: {token}?", + sep="\n", + ) except DuplicatedTeamError as e: if e.column == "chat_id": reply = text( @@ -96,32 +84,32 @@ async def register_team_h(message: types.Message, db_service: DBService) -> None async def update_team_h(message: types.Message, db_service: DBService) -> None: - updated_team_info = parse_msg_with_team_info(message) + current_team_info = await db_service.get_team_by_chat(message.chat.id) - if updated_team_info is None: - reply = text( - "Пожалуйста, введите данные в корректном формате.", - "/update_team team_name api_host api_key (опционально)", - ) - await message.reply(reply) - return + # TODO: think of way to generalize this pattern to reduce duplicate code + if current_team_info is None: + return await message.reply(TEAM_NOT_FOUND_MSG) - current_team_info = await db_service.get_team_by_chat(message.chat.id) + try: + update_field, update_value = message.get_args().split() + except ValueError: + return await message.reply(INCORRECT_DATA_IN_MSG) + + if update_field not in AVAILABLE_FOR_UPDATE: + return await message.reply(INCORRECT_DATA_IN_MSG) + + updated_team_info = TeamInfo(**current_team_info.dict()) + + setattr(updated_team_info, update_field, update_value) try: await db_service.update_team(current_team_info.team_id, updated_team_info) - reply = ( - "Данные по вашей команде были обновлены. Воспользуйтесь командой /show_current_team." + reply = text( + "Данные по вашей команде успешно обновлены.", + "Воспользуйтесь командой /show_current_team.", ) - except TeamNotFoundError: - reply = "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." except DuplicatedTeamError as e: - if e.column == "title": - reply = text( - f"Команда с именем `{updated_team_info.title}` уже существует.", - "Пожалуйста, выберите другое имя команды.", - ) - elif e.column == "api_base_url": + if e.column == "api_base_url": reply = text( f"Хост: `{updated_team_info.api_base_url}` уже кем-то используется.", "Пожалуйста, выберите другой хост.", @@ -139,17 +127,72 @@ async def show_current_team_h(message: types.Message, db_service: DBService) -> team_info = await db_service.get_team_by_chat(message.chat.id) api_key = team_info.api_key if team_info.api_key is not None else "Отсутствует" reply = text( - f"{bold('Команда')}: {team_info.title}", - f"{bold('Хост')}: {team_info.api_base_url}", - f"{bold('API Токен')}: {api_key}", + f"{bold('Команда')}: {escape_md(team_info.title)}", + f"{bold('Хост')}: {escape_md(team_info.api_base_url)}", + f"{bold('API Токен')}: {escape_md(api_key)}", sep="\n", ) except TeamNotFoundError: + reply = TEAM_NOT_FOUND_MSG + + await message.reply(reply, parse_mode=ParseMode.MARKDOWN_V2) + + +async def add_model_h(message: types.Message, db_service: DBService) -> None: + name, description = parse_msg_with_model_info(message) + + if name is None: + return await message.reply(INCORRECT_DATA_IN_MSG) + + team = await db_service.get_team_by_chat(message.chat.id) + + if team is None: + return await message.reply(TEAM_NOT_FOUND_MSG) + + try: + await db_service.add_model( + ModelInfo(team_id=team.team_id, name=name, description=description) + ) + reply = f"Модель `{name}` успешно добавлена. Воспользуйтесь командой /show_models" + except DuplicatedModelError: + reply = text( + "Модель с таким именем уже существует.", + "Пожалуйста, придумайте другое название для модели.", + ) + + await message.reply(reply) + + +async def show_models_h(message: types.Message, db_service: DBService) -> None: + team = await db_service.get_team_by_chat(message.chat.id) + + if team is None: + return await message.reply(TEAM_NOT_FOUND_MSG) + + models = await db_service.get_team_models(team.team_id) + + if len(models) == 0: + reply = "У вашей команды пока еще нет добавленных моделей" + else: + # TODO: get this filters in sql query + models.sort(key=lambda x: x.created_at, reverse=True) + models = models[:10] + dt_fmt = "%Y-%m-%d %H:%M:%S" reply = text( - "Команда от вашега чата не найдена. Скорее всего, что вы еще не регистрировались." + "Название модели, Описание, Дата добавления", + "\n".join( + f"{model.name}, {model.description}, {model.created_at.strftime(dt_fmt)}" + for model in models + ), + sep="\n", ) - await message.reply(reply, parse_mode=ParseMode.MARKDOWN) + await message.reply(reply) + + +# TODO: create request handler +async def request_h(message: types.Message, db_service: DBService) -> None: + raise NotImplementedError async def other_messages_h(message: types.Message, db_service: DBService) -> None: @@ -158,15 +201,19 @@ async def other_messages_h(message: types.Message, db_service: DBService) -> Non def register_handlers(dp: Dispatcher, db_service: DBService, config: ServiceConfig) -> None: bot_name = config.telegram_config.bot_name + # TODO: probably automate this dict with getting attributes from globals command_handlers_mapping = { - "start": start_h, - "help": help_h, - "register_team": register_team_h, - "update_team": update_team_h, - "show_current_team": show_current_team_h, + BotCommands.start.name: start_h, + BotCommands.help.name: help_h, + BotCommands.register_team.name: register_team_h, + BotCommands.update_team.name: update_team_h, + BotCommands.show_current_team.name: show_current_team_h, + BotCommands.add_model.name: add_model_h, + BotCommands.show_models.name: show_models_h, } for command, handler in command_handlers_mapping.items(): + # TODO: think of way to remove partial dp.register_message_handler(partial(handle, handler, db_service), commands=[command]) dp.register_message_handler( diff --git a/requestor/db/__init__.py b/requestor/db/__init__.py index f0c644a..19ea4a1 100644 --- a/requestor/db/__init__.py +++ b/requestor/db/__init__.py @@ -4,6 +4,7 @@ DuplicatedTeamError, ModelNotFoundError, TeamNotFoundError, + TokenNotFoundError, TrialNotFoundError, ) from .service import DBService @@ -15,5 +16,6 @@ "TeamNotFoundError", "ModelNotFoundError", "TrialNotFoundError", + "TokenNotFoundError", "DBService", ) diff --git a/requestor/log.py b/requestor/log.py index 7a264f2..b3fa633 100644 --- a/requestor/log.py +++ b/requestor/log.py @@ -51,7 +51,6 @@ def get_config(service_config: ServiceConfig) -> tp.Dict[str, tp.Any]: "format": ( 'time="%(asctime)s" ' 'level="%(levelname)s" ' - 'service_name="%(service_name)s" ' 'logger="%(name)s" ' 'pid="%(process)d" ' 'request_id="%(request_id)s" ' @@ -61,7 +60,7 @@ def get_config(service_config: ServiceConfig) -> tp.Dict[str, tp.Any]: }, }, "filters": { - "request_id": {"()": "reports_service.log.RequestIDFilter"}, + "request_id": {"()": "requestor.log.RequestIDFilter"}, }, } From c242e72a4cd933283721d694433bb01e3f83b6d0 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 05:11:35 +0300 Subject: [PATCH 05/15] polishing code --- requestor/bot/constants.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requestor/bot/constants.py b/requestor/bot/constants.py index 7d31910..0c7c31f 100644 --- a/requestor/bot/constants.py +++ b/requestor/bot/constants.py @@ -5,9 +5,8 @@ "api_key", } - INCORRECT_DATA_IN_MSG: tp.Final = ( - "Пожалуйста, введите данные в корректном формате. " "Используйте команду /help для справки." + "Пожалуйста, введите данные в корректном формате. Используйте команду /help для справки." ) TEAM_NOT_FOUND_MSG: tp.Final = ( From 31f0f8445fd39fb3051ad4936e733747f34fb039 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 14:47:00 +0300 Subject: [PATCH 06/15] refactored code --- main.py | 4 ++-- requestor/bot/commands.py | 25 ++++++++++++++----------- requestor/bot/handlers.py | 34 ++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/main.py b/main.py index 94ff0ba..cdf058f 100644 --- a/main.py +++ b/main.py @@ -2,13 +2,13 @@ from requestor.bot import dp, config, register_handlers, bot, BotCommands from requestor.services import make_db_service -from requestor.log import setup_logging, app_logger +from requestor.log import setup_logging async def main(): db_service = make_db_service(config) register_handlers(dp, db_service, config) setup_logging(config) - + await bot.set_my_commands(commands=BotCommands.get_bot_commands()) await db_service.setup() try: diff --git a/requestor/bot/commands.py b/requestor/bot/commands.py index a9da9df..8bfacf5 100644 --- a/requestor/bot/commands.py +++ b/requestor/bot/commands.py @@ -29,11 +29,11 @@ class CommandDescription: "С помощью этой команды можно зарегистрировать свою команду", "Принимает на вход аргументы через пробел:", "token - токен, который генерируется индивидуально для каждой команды.", - "title - название команды, без пробелов.", + "title - название команды, без пробелов и кавычек.", "api_base_url - хост, по которому будет находиться API команды.", "api_key - опционально, токен для запрашивания API.", "Пример использования:", - "/register_team 123 MyTeamName http://myapi.ru/api/v1 MyApiKey", + "/register_team MyToken MyTeamName http://myapi.ru/api/v1 MyApiKey", sep="\n", ), ), @@ -51,25 +51,27 @@ class CommandDescription: ), ), ( - "show_current_team", - "Вывод информацию по текущей команде", - "Выводит название команды, хоста и API токена", + "show_team", + "Вывод информации по текущей команде", + "Выводит название команды, хост и API токен", ), ( "add_model", "Добавление новой модели", text( - "С помощью этой команды можно добавить модели для проверки.", + "С помощью этой команды можно добавить модель для проверки.", "Для этого используются следующие аргументы:", - "name - хост, по которому будет находиться API команды.", + "name - название модели, без пробелов и кавычек.", "description - опционально, более подробное описание модели", - "Пример использования для обновления хоста:", + "Пример использования для добавления модели:", "/add_model lightfm_64", - ("Далее модели будут запрашиваться по адресу: " "{api_base_url}/{name}/{user_id}"), + "Далее модели будут запрашиваться по адресу: {api_base_url}/{name}/{user_id}", ( "То есть адрес для запроса выглядит, например, так: " - "http://myapi.ru/api/v1/lightfm_64/123" + "http://myapi.ru/api/v1/lightfm_64/178" ), + "Пример использования для добавления модели с описанием:", + "/add_model lightfm_64 Добавили фичи по юзерам и айтемам", sep="\n", ), ), @@ -80,6 +82,7 @@ class CommandDescription: "С помощью этой команды можно вывести следующую информацию:", "Название, описание (если присутствует) и дату добавления модели." "Если было добавлено более 10 моделей, то выведутся последние 10 по дате добавления", + "Время Московское", sep="\n", ), ), @@ -100,7 +103,7 @@ class BotCommands(Enum): help: CommandDescription = cmd2cls_desc["help"] register_team: CommandDescription = cmd2cls_desc["register_team"] update_team: CommandDescription = cmd2cls_desc["update_team"] - show_current_team: CommandDescription = cmd2cls_desc["show_current_team"] + show_team: CommandDescription = cmd2cls_desc["show_team"] add_model: CommandDescription = cmd2cls_desc["add_model"] show_models: CommandDescription = cmd2cls_desc["show_models"] diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py index 978f99a..2077895 100644 --- a/requestor/bot/handlers.py +++ b/requestor/bot/handlers.py @@ -4,6 +4,7 @@ from aiogram import Dispatcher, types from aiogram.types import ParseMode from aiogram.utils.markdown import bold, escape_md, text +from datetime import timedelta from requestor.db import ( DBService, @@ -106,7 +107,7 @@ async def update_team_h(message: types.Message, db_service: DBService) -> None: await db_service.update_team(current_team_info.team_id, updated_team_info) reply = text( "Данные по вашей команде успешно обновлены.", - "Воспользуйтесь командой /show_current_team.", + "Воспользуйтесь командой /show_team.", ) except DuplicatedTeamError as e: if e.column == "api_base_url": @@ -122,7 +123,7 @@ async def update_team_h(message: types.Message, db_service: DBService) -> None: await message.reply(reply) -async def show_current_team_h(message: types.Message, db_service: DBService) -> None: +async def show_team_h(message: types.Message, db_service: DBService) -> None: try: team_info = await db_service.get_team_by_chat(message.chat.id) api_key = team_info.api_key if team_info.api_key is not None else "Отсутствует" @@ -172,23 +173,28 @@ async def show_models_h(message: types.Message, db_service: DBService) -> None: models = await db_service.get_team_models(team.team_id) if len(models) == 0: - reply = "У вашей команды пока еще нет добавленных моделей" + return await message.reply("У вашей команды пока еще нет добавленных моделей") else: # TODO: get this filters in sql query models.sort(key=lambda x: x.created_at, reverse=True) - models = models[:10] + models = models[:5] dt_fmt = "%Y-%m-%d %H:%M:%S" - reply = text( - "Название модели, Описание, Дата добавления", - "\n".join( - f"{model.name}, {model.description}, {model.created_at.strftime(dt_fmt)}" - for model in models - ), - sep="\n", - ) + model_descriptions = [] + for model_num, model in enumerate(models, 1): + msc_time = model.created_at + timedelta(hours=3) + description = "Отсутствует" if model.description is None else model.description + model_description = text( + bold(f"Модель #{model_num}"), + f"{bold('Название')}: {escape_md(model.name)}", + f"{bold('Описание')}: {escape_md(description)}", + f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(dt_fmt))}", + sep="\n", + ) + model_descriptions.append(model_description) - await message.reply(reply) + reply = "\n\n".join(model_descriptions) + await message.reply(reply, parse_mode=ParseMode.MARKDOWN_V2) # TODO: create request handler async def request_h(message: types.Message, db_service: DBService) -> None: @@ -207,7 +213,7 @@ def register_handlers(dp: Dispatcher, db_service: DBService, config: ServiceConf BotCommands.help.name: help_h, BotCommands.register_team.name: register_team_h, BotCommands.update_team.name: update_team_h, - BotCommands.show_current_team.name: show_current_team_h, + BotCommands.show_team.name: show_team_h, BotCommands.add_model.name: add_model_h, BotCommands.show_models.name: show_models_h, } From 809e0a3727521da9ebeaab3b7bb1af5c7858f75b Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 15:21:13 +0300 Subject: [PATCH 07/15] changed add_team method to have parameter limit --- requestor/bot/bot_utils.py | 28 +++++++++++++++++++++++++--- requestor/bot/commands.py | 10 +++++++--- requestor/bot/constants.py | 4 ++++ requestor/bot/handlers.py | 24 ++++-------------------- requestor/db/service.py | 8 ++++++-- tests/db/test_service.py | 18 +++++++++++++----- 6 files changed, 59 insertions(+), 33 deletions(-) diff --git a/requestor/bot/bot_utils.py b/requestor/bot/bot_utils.py index 1cf0a5c..a521bfb 100644 --- a/requestor/bot/bot_utils.py +++ b/requestor/bot/bot_utils.py @@ -1,9 +1,10 @@ import typing as tp from aiogram import types - -from requestor.models import TeamInfo - +from aiogram.utils.markdown import bold, escape_md, text +from requestor.models import Model, TeamInfo +from datetime import timedelta +from .constants import DATE_FORMAT # TODO: somehow try generalize this func to reduce duplicate code def parse_msg_with_team_info( @@ -40,3 +41,24 @@ def parse_msg_with_model_info( return name, description except NameError: return None, None + +def generate_model_description(model: Model, model_num: int) -> str: + msc_time = model.created_at + timedelta(hours=3) + description = "Отсутствует" if model.description is None else model.description + return text( + bold(f"Модель #{model_num}"), + f"{bold('Название')}: {escape_md(model.name)}", + f"{bold('Описание')}: {escape_md(description)}", + f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(DATE_FORMAT))}", + sep="\n", + ) + + +def generate_models_description(models: tp.List[Model]) -> tp.List[str]: + model_descriptions = [] + for model_num, model in enumerate(models, 1): + model_description = generate_model_description(model, model_num) + model_descriptions.append(model_description) + + reply = "\n\n".join(model_descriptions) + return reply diff --git a/requestor/bot/commands.py b/requestor/bot/commands.py index 8bfacf5..088b0c3 100644 --- a/requestor/bot/commands.py +++ b/requestor/bot/commands.py @@ -4,6 +4,7 @@ from aiogram.types import BotCommand from aiogram.utils.markdown import text +from .constants import TEAM_MODELS_DISPLAY_LIMIT @dataclass @@ -80,9 +81,12 @@ class CommandDescription: "Вывод информации по добавленным моделям", text( "С помощью этой команды можно вывести следующую информацию:", - "Название, описание (если присутствует) и дату добавления модели." - "Если было добавлено более 10 моделей, то выведутся последние 10 по дате добавления", - "Время Московское", + ( + "Название, описание (если присутствует) и дату добавления модели по МСК. " + f"Если было добавлено более {TEAM_MODELS_DISPLAY_LIMIT} моделей, " + f"то выведутся последние {TEAM_MODELS_DISPLAY_LIMIT} по дате добавления " + "в обратном хронологическом порядке." + ), sep="\n", ), ), diff --git a/requestor/bot/constants.py b/requestor/bot/constants.py index 0c7c31f..7addac7 100644 --- a/requestor/bot/constants.py +++ b/requestor/bot/constants.py @@ -12,3 +12,7 @@ TEAM_NOT_FOUND_MSG: tp.Final = ( "Команда от вашего чата не найдена. Скорее всего, вы еще не регистрировались." ) + +TEAM_MODELS_DISPLAY_LIMIT: tp.Final = 20 + +DATE_FORMAT: tp.Final = "%Y-%m-%d %H:%M:%S" \ No newline at end of file diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py index 2077895..47b3b6b 100644 --- a/requestor/bot/handlers.py +++ b/requestor/bot/handlers.py @@ -17,9 +17,9 @@ from requestor.models import ModelInfo, TeamInfo from requestor.settings import ServiceConfig -from .bot_utils import parse_msg_with_model_info, parse_msg_with_team_info +from .bot_utils import parse_msg_with_model_info, parse_msg_with_team_info, generate_models_description from .commands import BotCommands -from .constants import AVAILABLE_FOR_UPDATE, INCORRECT_DATA_IN_MSG, TEAM_NOT_FOUND_MSG +from .constants import AVAILABLE_FOR_UPDATE, DATE_FORMAT, INCORRECT_DATA_IN_MSG, TEAM_MODELS_DISPLAY_LIMIT, TEAM_NOT_FOUND_MSG async def handle(handler, db_service: DBService, message: types.Message) -> None: @@ -170,29 +170,13 @@ async def show_models_h(message: types.Message, db_service: DBService) -> None: if team is None: return await message.reply(TEAM_NOT_FOUND_MSG) - models = await db_service.get_team_models(team.team_id) + models = await db_service.get_team_last_n_models(team.team_id, TEAM_MODELS_DISPLAY_LIMIT) if len(models) == 0: return await message.reply("У вашей команды пока еще нет добавленных моделей") else: # TODO: get this filters in sql query - models.sort(key=lambda x: x.created_at, reverse=True) - models = models[:5] - dt_fmt = "%Y-%m-%d %H:%M:%S" - model_descriptions = [] - for model_num, model in enumerate(models, 1): - msc_time = model.created_at + timedelta(hours=3) - description = "Отсутствует" if model.description is None else model.description - model_description = text( - bold(f"Модель #{model_num}"), - f"{bold('Название')}: {escape_md(model.name)}", - f"{bold('Описание')}: {escape_md(description)}", - f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(dt_fmt))}", - sep="\n", - ) - model_descriptions.append(model_description) - - reply = "\n\n".join(model_descriptions) + reply = generate_models_description(models) await message.reply(reply, parse_mode=ParseMode.MARKDOWN_V2) diff --git a/requestor/db/service.py b/requestor/db/service.py index 18d9b5c..77e2d8f 100644 --- a/requestor/db/service.py +++ b/requestor/db/service.py @@ -166,11 +166,15 @@ async def add_model(self, model_info: ModelInfo) -> Model: except ForeignKeyViolationError: raise TeamNotFoundError() - async def get_team_models(self, team_id: UUID) -> tp.List[Model]: - query = """ + async def get_team_last_n_models(self, team_id: UUID, limit: int) -> tp.List[Model]: + if limit <= 0: + raise ValueError(f"Parameter 'limit' should be positive, but got: {limit}") + query = f""" SELECT * FROM models WHERE team_id = $1::UUID + ORDER BY created_at DESC + LIMIT {limit} """ records = await self.pool.fetch(query, team_id) return [Model(**record) for record in records] diff --git a/tests/db/test_service.py b/tests/db/test_service.py index f21b466..54a9da1 100644 --- a/tests/db/test_service.py +++ b/tests/db/test_service.py @@ -1,5 +1,6 @@ # pylint: disable=attribute-defined-outside-init from datetime import timedelta +from multiprocessing.sharedctypes import Value from uuid import uuid4 import pytest @@ -228,7 +229,7 @@ async def test_add_model_for_nonexistent_team( db_teams = db_session.query(TeamsTable).all() assert len(db_teams) == 0 - async def test_get_team_models_success( + async def test_get_team_last_n_models_success( self, db_service: DBService, create_db_object: DBObjectCreator ) -> None: team_id = add_team(TEAM_INFO, create_db_object) @@ -237,17 +238,24 @@ async def test_get_team_models_success( model_2_info = gen_model_info(team_id, rnd="2") add_model(model_2_info, create_db_object) - models = await db_service.get_team_models(team_id) - assert sorted(m.name for m in models) == [model_1_info.name, model_2_info.name] + models = await db_service.get_team_last_n_models(team_id, 2) + assert sorted([m.name for m in models], reverse=True) == [model_2_info.name, model_1_info.name] - async def test_get_team_models_when_no_models( + async def test_get_team_last_n_models_when_no_models( self, db_service: DBService, create_db_object: DBObjectCreator ) -> None: team_id = add_team(TEAM_INFO, create_db_object) - models = await db_service.get_team_models(team_id) + models = await db_service.get_team_last_n_models(team_id, 1) assert len(models) == 0 + async def test_get_team_last_n_models_negative_limit( + self, db_service: DBService, create_db_object: DBObjectCreator + ) -> None: + team_id = add_team(TEAM_INFO, create_db_object) + with pytest.raises(ValueError): + await db_service.get_team_last_n_models(team_id, 0) + class TestTrials: @pytest.mark.parametrize("status", (TrialStatus.started, TrialStatus.waiting)) From 30701162be2c8eb08c623cd03223191319bb8574 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 15:29:51 +0300 Subject: [PATCH 08/15] make format/lint --- requestor/bot/bot_utils.py | 10 +++++++--- requestor/bot/commands.py | 1 + requestor/bot/constants.py | 4 ++-- requestor/bot/handlers.py | 17 +++++++++++++---- requestor/db/service.py | 6 +++--- tests/db/test_service.py | 6 ++++-- 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/requestor/bot/bot_utils.py b/requestor/bot/bot_utils.py index a521bfb..9e8ae62 100644 --- a/requestor/bot/bot_utils.py +++ b/requestor/bot/bot_utils.py @@ -1,11 +1,14 @@ import typing as tp +from datetime import timedelta from aiogram import types from aiogram.utils.markdown import bold, escape_md, text + from requestor.models import Model, TeamInfo -from datetime import timedelta + from .constants import DATE_FORMAT + # TODO: somehow try generalize this func to reduce duplicate code def parse_msg_with_team_info( message: types.Message, @@ -42,9 +45,10 @@ def parse_msg_with_model_info( except NameError: return None, None + def generate_model_description(model: Model, model_num: int) -> str: msc_time = model.created_at + timedelta(hours=3) - description = "Отсутствует" if model.description is None else model.description + description = "Отсутствует" if model.description is None else model.description return text( bold(f"Модель #{model_num}"), f"{bold('Название')}: {escape_md(model.name)}", @@ -54,7 +58,7 @@ def generate_model_description(model: Model, model_num: int) -> str: ) -def generate_models_description(models: tp.List[Model]) -> tp.List[str]: +def generate_models_description(models: tp.List[Model]) -> str: model_descriptions = [] for model_num, model in enumerate(models, 1): model_description = generate_model_description(model, model_num) diff --git a/requestor/bot/commands.py b/requestor/bot/commands.py index 088b0c3..e758e92 100644 --- a/requestor/bot/commands.py +++ b/requestor/bot/commands.py @@ -4,6 +4,7 @@ from aiogram.types import BotCommand from aiogram.utils.markdown import text + from .constants import TEAM_MODELS_DISPLAY_LIMIT diff --git a/requestor/bot/constants.py b/requestor/bot/constants.py index 7addac7..fdcdfd8 100644 --- a/requestor/bot/constants.py +++ b/requestor/bot/constants.py @@ -13,6 +13,6 @@ "Команда от вашего чата не найдена. Скорее всего, вы еще не регистрировались." ) -TEAM_MODELS_DISPLAY_LIMIT: tp.Final = 20 +TEAM_MODELS_DISPLAY_LIMIT: tp.Final = 10 -DATE_FORMAT: tp.Final = "%Y-%m-%d %H:%M:%S" \ No newline at end of file +DATE_FORMAT: tp.Final = "%Y-%m-%d %H:%M:%S" diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py index 47b3b6b..a4f9a4c 100644 --- a/requestor/bot/handlers.py +++ b/requestor/bot/handlers.py @@ -4,7 +4,6 @@ from aiogram import Dispatcher, types from aiogram.types import ParseMode from aiogram.utils.markdown import bold, escape_md, text -from datetime import timedelta from requestor.db import ( DBService, @@ -17,9 +16,18 @@ from requestor.models import ModelInfo, TeamInfo from requestor.settings import ServiceConfig -from .bot_utils import parse_msg_with_model_info, parse_msg_with_team_info, generate_models_description +from .bot_utils import ( + generate_models_description, + parse_msg_with_model_info, + parse_msg_with_team_info, +) from .commands import BotCommands -from .constants import AVAILABLE_FOR_UPDATE, DATE_FORMAT, INCORRECT_DATA_IN_MSG, TEAM_MODELS_DISPLAY_LIMIT, TEAM_NOT_FOUND_MSG +from .constants import ( + AVAILABLE_FOR_UPDATE, + INCORRECT_DATA_IN_MSG, + TEAM_MODELS_DISPLAY_LIMIT, + TEAM_NOT_FOUND_MSG, +) async def handle(handler, db_service: DBService, message: types.Message) -> None: @@ -173,13 +181,14 @@ async def show_models_h(message: types.Message, db_service: DBService) -> None: models = await db_service.get_team_last_n_models(team.team_id, TEAM_MODELS_DISPLAY_LIMIT) if len(models) == 0: - return await message.reply("У вашей команды пока еще нет добавленных моделей") + reply = "У вашей команды пока еще нет добавленных моделей" else: # TODO: get this filters in sql query reply = generate_models_description(models) await message.reply(reply, parse_mode=ParseMode.MARKDOWN_V2) + # TODO: create request handler async def request_h(message: types.Message, db_service: DBService) -> None: raise NotImplementedError diff --git a/requestor/db/service.py b/requestor/db/service.py index 77e2d8f..26039c1 100644 --- a/requestor/db/service.py +++ b/requestor/db/service.py @@ -169,14 +169,14 @@ async def add_model(self, model_info: ModelInfo) -> Model: async def get_team_last_n_models(self, team_id: UUID, limit: int) -> tp.List[Model]: if limit <= 0: raise ValueError(f"Parameter 'limit' should be positive, but got: {limit}") - query = f""" + query = """ SELECT * FROM models WHERE team_id = $1::UUID ORDER BY created_at DESC - LIMIT {limit} + LIMIT $2::BIGINT """ - records = await self.pool.fetch(query, team_id) + records = await self.pool.fetch(query, team_id, limit) return [Model(**record) for record in records] async def add_trial(self, model_id: UUID, status: TrialStatus) -> Trial: diff --git a/tests/db/test_service.py b/tests/db/test_service.py index 54a9da1..1e60169 100644 --- a/tests/db/test_service.py +++ b/tests/db/test_service.py @@ -1,6 +1,5 @@ # pylint: disable=attribute-defined-outside-init from datetime import timedelta -from multiprocessing.sharedctypes import Value from uuid import uuid4 import pytest @@ -239,7 +238,10 @@ async def test_get_team_last_n_models_success( add_model(model_2_info, create_db_object) models = await db_service.get_team_last_n_models(team_id, 2) - assert sorted([m.name for m in models], reverse=True) == [model_2_info.name, model_1_info.name] + assert sorted([m.name for m in models], reverse=True) == [ + model_2_info.name, + model_1_info.name, + ] async def test_get_team_last_n_models_when_no_models( self, db_service: DBService, create_db_object: DBObjectCreator From b1bde4e747afd229b71e8b589b4b1b3cb43f1c39 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 15:48:51 +0300 Subject: [PATCH 09/15] fixed test_last_n_models_success --- tests/db/test_service.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/db/test_service.py b/tests/db/test_service.py index 1e60169..f524a1a 100644 --- a/tests/db/test_service.py +++ b/tests/db/test_service.py @@ -238,10 +238,7 @@ async def test_get_team_last_n_models_success( add_model(model_2_info, create_db_object) models = await db_service.get_team_last_n_models(team_id, 2) - assert sorted([m.name for m in models], reverse=True) == [ - model_2_info.name, - model_1_info.name, - ] + assert [m.name for m in models] == [model_2_info.name, model_1_info.name] async def test_get_team_last_n_models_when_no_models( self, db_service: DBService, create_db_object: DBObjectCreator From eeb2262ea89f315203d2548f0e589dae6508da26 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 16:46:00 +0300 Subject: [PATCH 10/15] added current time when adding data to db, instead of the same --- tests/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 22d34df..1c8b7ec 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -145,7 +145,9 @@ def add_model( create_db_object: DBObjectCreator, ) -> UUID: model_id = uuid4() - create_db_object(make_db_model(**model_info.dict(), model_id=model_id)) + create_db_object( + make_db_model(**model_info.dict(), model_id=model_id, created_at=datetime.now()) + ) return model_id From 47bcb71153138acd8bb7a3a217829645d43ae743 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sat, 22 Oct 2022 16:58:20 +0300 Subject: [PATCH 11/15] removed TODO about SQL --- requestor/bot/handlers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/requestor/bot/handlers.py b/requestor/bot/handlers.py index a4f9a4c..d4da97d 100644 --- a/requestor/bot/handlers.py +++ b/requestor/bot/handlers.py @@ -183,7 +183,6 @@ async def show_models_h(message: types.Message, db_service: DBService) -> None: if len(models) == 0: reply = "У вашей команды пока еще нет добавленных моделей" else: - # TODO: get this filters in sql query reply = generate_models_description(models) await message.reply(reply, parse_mode=ParseMode.MARKDOWN_V2) From 3477614d88570ea6ed2fdc5302b26c238bcb19f2 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sun, 23 Oct 2022 12:20:05 +0300 Subject: [PATCH 12/15] added 3rd model to test last_n_models --- tests/db/test_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/db/test_service.py b/tests/db/test_service.py index f524a1a..a4fae4c 100644 --- a/tests/db/test_service.py +++ b/tests/db/test_service.py @@ -236,9 +236,11 @@ async def test_get_team_last_n_models_success( add_model(model_1_info, create_db_object) model_2_info = gen_model_info(team_id, rnd="2") add_model(model_2_info, create_db_object) + model_3_info = gen_model_info(team_id, rnd="3") + add_model(model_3_info, create_db_object) models = await db_service.get_team_last_n_models(team_id, 2) - assert [m.name for m in models] == [model_2_info.name, model_1_info.name] + assert [m.name for m in models] == [model_3_info.name, model_2_info.name] async def test_get_team_last_n_models_when_no_models( self, db_service: DBService, create_db_object: DBObjectCreator From 20ad87f5a69b3cf107573e2a23b8714a7aa0f00d Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sun, 23 Oct 2022 12:22:41 +0300 Subject: [PATCH 13/15] changed model description --- requestor/bot/bot_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requestor/bot/bot_utils.py b/requestor/bot/bot_utils.py index 9e8ae62..268bd56 100644 --- a/requestor/bot/bot_utils.py +++ b/requestor/bot/bot_utils.py @@ -50,7 +50,7 @@ def generate_model_description(model: Model, model_num: int) -> str: msc_time = model.created_at + timedelta(hours=3) description = "Отсутствует" if model.description is None else model.description return text( - bold(f"Модель #{model_num}"), + bold(model_num), f"{bold('Название')}: {escape_md(model.name)}", f"{bold('Описание')}: {escape_md(description)}", f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(DATE_FORMAT))}", From 11a4cc8ee976b76ee407337b09767ee803ee9c8b Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sun, 23 Oct 2022 12:23:35 +0300 Subject: [PATCH 14/15] changed variable name of datetime format --- requestor/bot/bot_utils.py | 4 ++-- requestor/bot/constants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requestor/bot/bot_utils.py b/requestor/bot/bot_utils.py index 268bd56..dc52208 100644 --- a/requestor/bot/bot_utils.py +++ b/requestor/bot/bot_utils.py @@ -6,7 +6,7 @@ from requestor.models import Model, TeamInfo -from .constants import DATE_FORMAT +from .constants import DATETIME_FORMAT # TODO: somehow try generalize this func to reduce duplicate code @@ -53,7 +53,7 @@ def generate_model_description(model: Model, model_num: int) -> str: bold(model_num), f"{bold('Название')}: {escape_md(model.name)}", f"{bold('Описание')}: {escape_md(description)}", - f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(DATE_FORMAT))}", + f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(DATETIME_FORMAT))}", sep="\n", ) diff --git a/requestor/bot/constants.py b/requestor/bot/constants.py index fdcdfd8..f954baa 100644 --- a/requestor/bot/constants.py +++ b/requestor/bot/constants.py @@ -15,4 +15,4 @@ TEAM_MODELS_DISPLAY_LIMIT: tp.Final = 10 -DATE_FORMAT: tp.Final = "%Y-%m-%d %H:%M:%S" +DATETIME_FORMAT: tp.Final = "%Y-%m-%d %H:%M:%S" From 30733fccab525570a1a06b7950dea420d47a6323 Mon Sep 17 00:00:00 2001 From: mlukin1 Date: Sun, 23 Oct 2022 12:33:11 +0300 Subject: [PATCH 15/15] changed time to UTC when generating model description --- requestor/bot/bot_utils.py | 7 +++---- requestor/bot/commands.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/requestor/bot/bot_utils.py b/requestor/bot/bot_utils.py index dc52208..a643b42 100644 --- a/requestor/bot/bot_utils.py +++ b/requestor/bot/bot_utils.py @@ -1,5 +1,4 @@ import typing as tp -from datetime import timedelta from aiogram import types from aiogram.utils.markdown import bold, escape_md, text @@ -47,13 +46,13 @@ def parse_msg_with_model_info( def generate_model_description(model: Model, model_num: int) -> str: - msc_time = model.created_at + timedelta(hours=3) - description = "Отсутствует" if model.description is None else model.description + description = model.description or "Отсутствует" + created_at = model.created_at.strftime(DATETIME_FORMAT) return text( bold(model_num), f"{bold('Название')}: {escape_md(model.name)}", f"{bold('Описание')}: {escape_md(description)}", - f"{bold('Дата добавления по МСК')}: {escape_md(msc_time.strftime(DATETIME_FORMAT))}", + f"{bold('Дата добавления (UTC)')}: {escape_md(created_at)}", sep="\n", ) diff --git a/requestor/bot/commands.py b/requestor/bot/commands.py index e758e92..8b24160 100644 --- a/requestor/bot/commands.py +++ b/requestor/bot/commands.py @@ -83,7 +83,7 @@ class CommandDescription: text( "С помощью этой команды можно вывести следующую информацию:", ( - "Название, описание (если присутствует) и дату добавления модели по МСК. " + "Название, описание (если присутствует) и дату добавления модели по UTC. " f"Если было добавлено более {TEAM_MODELS_DISPLAY_LIMIT} моделей, " f"то выведутся последние {TEAM_MODELS_DISPLAY_LIMIT} по дате добавления " "в обратном хронологическом порядке."