diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f116a061..cbf746b86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## v3.3.0 - 2024-12-21 +### What's Changed +**Full Changelog**: https://github.com/obervinov/pyinstabot-downloader/compare/v3.2.0...v3.3.0 by @obervinov in https://github.com/obervinov/pyinstabot-downloader/pull/123 +#### ๐Ÿ› Bug Fixes +* small code formatting improvements +#### ๐Ÿš€ Features +* add new table `accounts` to the database for storing account public data +* add the feature to retrieve the entire list of account posts and add it to the queue +* [Feature request: move user rights checking from `if` to native `decorators`](https://github.com/obervinov/pyinstabot-downloader/issues/117) + + ## v3.2.0 - 2024-11-21 ### What's Changed **Full Changelog**: https://github.com/obervinov/pyinstabot-downloader/compare/v3.1.3...v3.2.0 by @obervinov in https://github.com/obervinov/pyinstabot-downloader/pull/116 diff --git a/poetry.lock b/poetry.lock index a22aeddb3..8ccf0b751 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,24 +13,24 @@ files = [ [[package]] name = "astroid" -version = "3.3.5" +version = "3.3.6" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, - {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, + {file = "astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f"}, + {file = "astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442"}, ] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -247,17 +247,17 @@ files = [ [[package]] name = "instagrapi" -version = "2.1.2" +version = "2.1.3" description = "Fast and effective Instagram Private API wrapper" optional = false python-versions = ">=3.9" files = [ - {file = "instagrapi-2.1.2.tar.gz", hash = "sha256:08779764e4f6a39f83cdb422d67f8bb724dcd10410349e815884536383d4c0c8"}, + {file = "instagrapi-2.1.3.tar.gz", hash = "sha256:b8b891c7163a42e53baaee8d4ec2ef21007d12a65d564c7064d3dabec17da34b"}, ] [package.dependencies] -pycryptodomex = "3.20.0" -pydantic = "2.7.1" +pycryptodomex = "3.21.0" +pydantic = "2.10.1" PySocks = "1.7.1" requests = ">=2.25.1,<3.0" @@ -491,13 +491,13 @@ test = ["pytest", "pytest-cov"] [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -625,13 +625,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" -version = "0.21.0" +version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, - {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, + {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, + {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, ] [package.extras] @@ -726,150 +726,172 @@ files = [ [[package]] name = "pycryptodomex" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, - {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, + {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, ] [[package]] name = "pydantic" -version = "2.7.1" +version = "2.10.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, + {file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"}, + {file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.18.2" -typing-extensions = ">=4.6.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.18.2" +version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, ] [package.dependencies] @@ -888,17 +910,17 @@ files = [ [[package]] name = "pylint" -version = "3.3.1" +version = "3.3.2" description = "python code static checker" optional = false python-versions = ">=3.9.0" files = [ - {file = "pylint-3.3.1-py3-none-any.whl", hash = "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9"}, - {file = "pylint-3.3.1.tar.gz", hash = "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e"}, + {file = "pylint-3.3.2-py3-none-any.whl", hash = "sha256:77f068c287d49b8683cd7c6e624243c74f92890f767f106ffa1ddf3c0a54cb7a"}, + {file = "pylint-3.3.2.tar.gz", hash = "sha256:9ec054ec992cd05ad30a6df1676229739a73f8feeabf3912c995d17601052b01"}, ] [package.dependencies] -astroid = ">=3.3.4,<=3.4.0-dev0" +astroid = ">=3.3.5,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" @@ -924,13 +946,13 @@ files = [ [[package]] name = "pytelegrambotapi" -version = "4.23.0" +version = "4.25.0" description = "Python Telegram bot api." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytelegrambotapi-4.23.0-py3-none-any.whl", hash = "sha256:4fd4a64f3d5ec389270cf4f1eacd68f6d25d199e1048b76a1caefcb17fbe214b"}, - {file = "pytelegrambotapi-4.23.0.tar.gz", hash = "sha256:ced74787cfaf59d959799786f12a401cdb3abeb58dcd25568fc91363ba1cccfa"}, + {file = "pytelegrambotapi-4.25.0-py3-none-any.whl", hash = "sha256:2ccdbf407671caa09880055f83c1ff7df9a88933b51708b948dac56fc2988829"}, + {file = "pytelegrambotapi-4.25.0.tar.gz", hash = "sha256:7b59969871cc008b3bc3526efb96d883b7c8eaca2a979e79b1e0269d27bf75e0"}, ] [package.dependencies] @@ -949,13 +971,13 @@ watchdog = ["watchdog"] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -1004,13 +1026,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -1075,7 +1097,7 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "users" -version = "4.0.1" +version = "4.0.2" description = "This python module is a simple implementation of user management functionality for telegram bots, such as: authentication, authorization and requests limiting." optional = false python-versions = "^3.12" @@ -1090,8 +1112,8 @@ vault = {git = "https://github.com/obervinov/vault-package.git", tag = "v4.0.0"} [package.source] type = "git" url = "https://github.com/obervinov/users-package.git" -reference = "v4.0.1" -resolved_reference = "028833a60221fc7949e7743e8e74bef264b7d21c" +reference = "v4.0.2" +resolved_reference = "b7d40d1c7e5fac81e2ef24e9e360396b4847c3eb" [[package]] name = "vault" @@ -1131,4 +1153,4 @@ requests = "*" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "cb0b52a56037f37461479a9e95905f1bcfdd042f220c07317a6db7d97b01f762" +content-hash = "ec3871446b8e479363eac65cd8c3504497fb9bb15d5ca992e6cdd5dc4c692d74" diff --git a/pyproject.toml b/pyproject.toml index 1e9efe0c6..e849e8090 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyinstabot-downloader" -version = "3.2.0" +version = "3.3.0" description = "This project is a Telegram bot that allows you to upload posts from your Instagram profile to clouds like any WebDav compatible cloud storage." authors = ["Bervinov Oleg "] maintainers = ["Bervinov Oleg "] @@ -26,7 +26,7 @@ webdavclient3 = "^3" prometheus-client = "^0" logger = { git = "https://github.com/obervinov/logger-package.git", tag = "v2.0.0" } vault = { git = "https://github.com/obervinov/vault-package.git", tag = "v4.0.0" } -users = { git = "https://github.com/obervinov/users-package.git", tag = "v4.0.1" } +users = { git = "https://github.com/obervinov/users-package.git", tag = "v4.0.2" } telegram = { git = "https://github.com/obervinov/telegram-package.git", tag = "v3.0.0" } [tool.poetry.group.dev.dependencies] diff --git a/src/bot.py b/src/bot.py index ebb7d3db9..955c5f8fd 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,3 +1,4 @@ +# pylint: disable=unused-argument """ This module contains the main code for the bot to work and contains the main logic linking the additional modules. """ @@ -14,8 +15,7 @@ from users import Users from vault import VaultClient from configs.constants import ( - TELEGRAM_BOT_NAME, ROLES_MAP, QUEUE_FREQUENCY, STATUSES_MESSAGE_FREQUENCY, - METRICS_PORT, METRICS_INTERVAL, VAULT_DB_ROLE + TELEGRAM_BOT_NAME, ROLES_MAP, QUEUE_FREQUENCY, STATUSES_MESSAGE_FREQUENCY, METRICS_PORT, METRICS_INTERVAL, VAULT_DB_ROLE ) from modules.database import DatabaseClient from modules.exceptions import FailedMessagesStatusUpdater @@ -28,9 +28,9 @@ # Vault client vault = VaultClient() # Telegram instance -telegram = TelegramBot(vault=vault) +tg = TelegramBot(vault=vault) # Telegram bot for decorators -bot = telegram.telegram_bot +bot = tg.telegram_bot # Client for communication with the database database = DatabaseClient(vault=vault, db_role=VAULT_DB_ROLE) # Metrics exporter @@ -50,10 +50,7 @@ log.warning('[Bot]: Downloader api is disabled, using mock object, because enabled flag is %s', downloader_api_enabled) downloader = MagicMock() downloader.get_post_content.return_value = { - 'post': f"mock_{''.join(random.choices(string.ascii_letters + string.digits, k=10))}", - 'owner': 'mock', - 'type': 'fake', - 'status': 'completed' + 'post': f"mock_{''.join(random.choices(string.ascii_letters + string.digits, k=10))}", 'owner': 'mock', 'type': 'fake', 'status': 'completed' } # Client for upload content to the target storage @@ -68,110 +65,194 @@ uploader.run_transfers.return_value = 'completed' -# START HANDLERS BLOCK ############################################################################################################## -# Command handler for START command +# Bot commands ##################################################################################################################### @bot.message_handler(commands=['start']) -def start_command(message: telegram.telegram_types.Message = None) -> None: +@users.access_control(flow='auth') +def start_command_handler(message: tg.telegram_types.Message, access_result: dict) -> None: """ - Sends a startup message to the specified Telegram chat. + Processes the main logic of the 'start' command under access control. Args: - message (telegram.telegram_types.Message): The message object containing information about the chat. + message (tg.telegram_types.Message): The message object containing chat information. + access_result (dict): The dictionary containing the access result. Propagated from the access_control decorator. """ - requestor = {'user_id': message.chat.id, 'chat_id': message.chat.id, 'message_id': message.message_id} - if users.user_access_check(**requestor).get('access', None) == users.user_status_allow: - log.info('[Bot]: Processing start command for user %s...', message.chat.id) - # Main pinned message - reply_markup = telegram.create_inline_markup(ROLES_MAP.keys()) - start_message = telegram.send_styled_message( - chat_id=message.chat.id, - messages_template={ - 'alias': 'start_message', - 'kwargs': {'username': message.from_user.username, 'userid': message.chat.id} - }, - reply_markup=reply_markup - ) - bot.pin_chat_message(start_message.chat.id, start_message.id) - bot.delete_message(message.chat.id, message.id) - update_status_message(user_id=message.chat.id) + log.info('[Bot]: Processing start command for user %s...', message.chat.id) + reply_markup = tg.create_inline_markup(ROLES_MAP.keys()) + start_message = tg.send_styled_message( + chat_id=message.chat.id, + messages_template={'alias': 'start_message', 'kwargs': {'username': message.from_user.username, 'userid': message.chat.id}}, + reply_markup=reply_markup, + ) + bot.pin_chat_message(start_message.chat.id, start_message.id) + bot.delete_message(message.chat.id, message.message_id) + update_status_message(user_id=message.chat.id) + + +# Callback query handler for InlineKeyboardButton +@bot.callback_query_handler(func=lambda call: True) +@users.access_control(flow='auth') +def bot_callback_query_handler(call: tg.callback_query, access_result: dict) -> None: + """ + Processes the button press from the user. + + Args: + call (tg.callback_query): The callback query + access_result (dict): The dictionary containing the access result. Propagated from the access_control decorator. + """ + log.info('[Bot]: Processing button %s for user %s...', call.data, call.message.chat.id) + alias = None + if call.data == "Posts": + alias = 'help_for_posts_list' + method = process_posts + elif call.data == "Account": + alias = 'help_for_account' + method = process_account + elif call.data == "Reschedule Queue": + alias = 'help_for_reschedule_queue' + method = reschedule_queue else: - telegram.send_styled_message( - chat_id=message.chat.id, - messages_template={ - 'alias': 'reject_message', - 'kwargs': {'username': message.chat.username, 'userid': message.chat.id} - } - ) + log.error('[Bot]: Handler for button %s not found', call.data) + alias = 'unknown_command' + method = None + help_message = tg.send_styled_message(chat_id=call.message.chat.id, messages_template={'alias': alias}) + bot.register_next_step_handler(call.message, method, help_message) -# Callback query handler for InlineKeyboardButton (BUTTONS) -@bot.callback_query_handler(func=lambda call: True) -def bot_callback_query_handler(call: telegram.callback_query = None) -> None: +# Button handlers ################################################################################################################### +@users_rl.access_control(flow='authz', role_id=ROLES_MAP['Posts']) +def post_code_handler(message: tg.telegram_types.Message, data: dict, access_result: dict = None) -> None: """ - The handler for the callback query from the user. - Mainly used to handle button presses. + Processes the post code from the user's message. Args: - call (telegram.callback_query): The callback query object. + message (tg.telegram_types.Message): Required for the access_control decorator. + data (dict): The dictionary containing the post code. + user_id (str): The telegram user id. + post_id (str): The post id (shortcode). + post_owner (str): The post owner. Defaults to 'undefined'. + link_type (str): The type of link. Defaults to 'post'. + message_id (str): The message id in the chat. + chat_id (str): The chat id in the chat. + access_result (dict): The dictionary containing the access result. Propagated from the access_control decorator. """ - log.info('[Bot]: Processing button %s for user %s...', call.data, call.message.chat.id) - requestor = { - 'user_id': call.message.chat.id, 'role_id': ROLES_MAP[call.data], - 'chat_id': call.message.chat.id, 'message_id': call.message.message_id - } - if users.user_access_check(**requestor).get('permissions', None) == users.user_status_allow: - if call.data == "Posts": - help_message = telegram.send_styled_message( - chat_id=call.message.chat.id, - messages_template={'alias': 'help_for_posts_list'} - ) - bot.register_next_step_handler(call.message, process_posts, help_message) + data['scheduled_time'] = access_result.get('rate_limits') or datetime.now() + if data['link_type'] == 'account': + # Delay for account parsing + data['scheduled_time'] += timedelta(minutes=60) + status = database.add_message_to_queue(data) + log.info('[Bot]: %s for user_id %s', status, data['user_id']) - elif call.data == "Reschedule Queue": - help_message = telegram.send_styled_message( - chat_id=call.message.chat.id, - messages_template={'alias': 'help_for_reschedule_queue'} - ) - bot.register_next_step_handler(call.message, reschedule_queue, help_message) +@users.access_control(flow='authz', role_id=ROLES_MAP['Posts']) +def process_posts(message: tg.telegram_types.Message, help_message: tg.telegram_types.Message, access_result: dict) -> None: + """ + Process a single or multiple posts from the user's message. + + Args: + message (telegram.telegram_types.Message, optional): The message containing the list of post links. Defaults to None. + help_message (telegram.telegram_types.Message, optional): The help message to be deleted. Defaults to None. + access_result (dict): The dictionary containing the access result. Propagated from the access_control decorator. + """ + cleanup_messages = True + for link in message.text.split('\n'): + # Verify that the link is a post link + if re.match(r'^https://www\.instagram\.com/(p|reel)/.*', message.text): + post_id = link.split('/')[4] + # Verify that the post id is correct + if len(post_id) == 11 and re.match(r'^[a-zA-Z0-9_-]+$', post_id): + if database.check_message_uniqueness(post_id=post_id, user_id=message.chat.id): + post_code_handler(message, data={ + 'user_id': message.chat.id, 'post_id': post_id, 'post_owner': 'undefined', 'link_type': 'post', + 'message_id': message.id, 'chat_id': message.chat.id, 'post_url': link.split('?')[0] + }) + else: + cleanup_messages = False + log.error('[Bot]: post id %s from user %s is wrong', post_id, message.chat.id) + tg.send_styled_message(chat_id=message.chat.id, messages_template={'alias': 'url_error', 'kwargs': {'url': message.text}}) else: - log.error('[Bot]: Handler for button %s not found', call.data) + cleanup_messages = False + log.error('[Bot]: post link %s from user %s is incorrect', message.text, message.chat.id) + tg.send_styled_message(chat_id=message.chat.id, messages_template={'alias': 'url_error'}) - else: - telegram.send_styled_message( - chat_id=call.message.chat.id, - messages_template={ - 'alias': 'permission_denied_message', - 'kwargs': {'username': call.message.chat.username, 'userid': call.message.chat.id} - } - ) + if cleanup_messages: + tg.delete_message(message.chat.id, message.id) + tg.delete_message(message.chat.id, help_message.id) -# Handler for incorrect flow (UNKNOWN INPUT) -@bot.message_handler(regexp=r'.*') -def unknown_command(message: telegram.telegram_types.Message = None) -> None: +@users.access_control(flow='authz', role_id=ROLES_MAP['Account']) +def process_account(message: tg.telegram_types.Message, help_message: tg.telegram_types.Message, access_result: dict) -> None: """ - Sends a message to the user if the command is not recognized. + Processes the user's account posts and adds them to the queue for download. Args: - message (telegram.telegram_types.Message): The message object containing the unrecognized command. + message (telegram.telegram_types.Message): The message object containing the user's account link. + help_message (telegram.telegram_types.Message, optional): The help message to be deleted. + access_result (dict): The dictionary containing the access result. Propagated from the access_control decorator. """ - requestor = {'user_id': message.chat.id, 'chat_id': message.chat.id, 'message_id': message.message_id} - if users.user_access_check(**requestor).get('access', None) == users.user_status_allow: - log.error('[Bot]: Invalid command %s from user %s', message.text, message.chat.id) - telegram.send_styled_message(chat_id=message.chat.id, messages_template={'alias': 'unknown_command'}) - else: - telegram.send_styled_message( - chat_id=message.chat.id, - messages_template={ - 'alias': 'reject_message', - 'kwargs': {'username': message.chat.username, 'userid': message.chat.id} - } - ) -# END HANDLERS BLOCK ############################################################################################################## + if re.match(r'^https://www\.instagram\.com/.*', message.text): + account_name = message.text.split('/')[3].split('?')[0] + account_id, cursor = database.get_account_info(username=account_name) + if not account_id: + log.info('[Bot]: account %s does not exist in the database, will request data from API', account_name) + account_info = downloader.get_account_info(username=account_name) + database.add_account_info(data=account_info) + account_id = account_info['pk'] + + while True: + posts_list, cursor = downloader.get_account_posts(user_id=account_id, cursor=cursor) + log.info('[Bot]: received %s posts from account %s', len(posts_list), account_name) + for post in posts_list: + if database.check_message_uniqueness(post_id=post.code, user_id=message.chat.id): + post_code_handler(message, data={ + 'user_id': message.chat.id, 'post_id': post.code, 'post_owner': account_name, 'link_type': 'account', + 'message_id': message.id, 'chat_id': message.chat.id, + 'post_url': f"https://www.instagram.com/{downloader.media_type_links[post.media_type]}/{post.code}" + }) + if not cursor: + log.info('[Bot]: full posts list from account %s retrieved', account_name) + tg.delete_message(message.chat.id, message.id) + tg.delete_message(message.chat.id, help_message.id) + break + database.add_account_info({'username': account_name, 'cursor': cursor}) + time.sleep(int(downloader.configuration['delay-requests']) * random.randint(5, 50)) + + +@users.access_control(flow='authz', role_id=ROLES_MAP['Reschedule Queue']) +def reschedule_queue(message: tg.telegram_types.Message, help_message: tg.telegram_types.Message, access_result: dict) -> None: + """ + Manually reschedules the queue for the user. + + Args: + message (telegram.telegram_types.Message, optional): The message containing the list of post links. Defaults to None. + help_message (telegram.telegram_types.Message, optional): The help message to be deleted. Defaults to None. + """ + can_be_deleted = True + for item in message.text.split('\n'): + item = item.split(': scheduled for ') + post_id = item[0].strip() + new_scheduled_time = datetime.strptime(item[1].strip(), '%Y-%m-%d %H:%M:%S.%f') + if ( + isinstance(post_id, str) and len(post_id) == 11 and + isinstance(new_scheduled_time, datetime) and new_scheduled_time > datetime.now() + ): + database.update_schedule_time_in_queue( + post_id=post_id, + user_id=message.chat.id, + scheduled_time=new_scheduled_time + ) + else: + can_be_deleted = False + tg.send_styled_message( + chat_id=message.chat.id, + messages_template={'alias': 'wrong_reschedule_queue', 'kwargs': {'current_time': datetime.now()}} + ) + if can_be_deleted: + tg.delete_message(message.chat.id, message.id) + if help_message is not None: + tg.delete_message(message.chat.id, help_message.id) -# START BLOCK ADDITIONAL FUNCTIONS ###################################################################################################### +# Internal methods ################################################################################################################# def update_status_message(user_id: str = None) -> None: """ Updates the status message for the user. @@ -206,13 +287,9 @@ def update_status_message(user_id: str = None) -> None: # automatic renew message every 24 hours if exist_status_message[2] < datetime.now() - timedelta(hours=24): if exist_status_message[2] > datetime.now() - timedelta(hours=48): - _ = bot.delete_message( - chat_id=user_id, - message_id=exist_status_message[0] - ) - status_message = telegram.send_styled_message( - chat_id=user_id, - messages_template={'alias': 'message_statuses', 'kwargs': message_statuses} + _ = bot.delete_message(chat_id=user_id, message_id=exist_status_message[0]) + status_message = tg.send_styled_message( + chat_id=user_id, messages_template={'alias': 'message_statuses', 'kwargs': message_statuses} ) database.keep_message( message_id=status_message.message_id, @@ -225,7 +302,7 @@ def update_status_message(user_id: str = None) -> None: log.info('[Bot]: `status_message` for user %s has been renewed', user_id) elif message_statuses is not None and diff_between_messages: - editable_message = telegram.send_styled_message( + editable_message = tg.send_styled_message( chat_id=user_id, messages_template={'alias': 'message_statuses', 'kwargs': message_statuses}, editable_message_id=exist_status_message[0] @@ -250,9 +327,8 @@ def update_status_message(user_id: str = None) -> None: ) else: - status_message = telegram.send_styled_message( - chat_id=user_id, - messages_template={'alias': 'message_statuses', 'kwargs': message_statuses} + status_message = tg.send_styled_message( + chat_id=user_id, messages_template={'alias': 'message_statuses', 'kwargs': message_statuses} ) database.keep_message( message_id=status_message.message_id, @@ -262,14 +338,11 @@ def update_status_message(user_id: str = None) -> None: ) log.info('[Bot]: `status_message` for user %s has been created', user_id) except TypeError as exception: - exception_context = { + log.error({ 'message': f"Failed to update the message with the status of received messages for user {user_id}", - 'exception': exception, - 'exist_status_message': exist_status_message, - 'message_statuses': message_statuses, + 'exception': exception, 'exist_status_message': exist_status_message, 'message_statuses': message_statuses, 'diff_between_messages': diff_between_messages - } - log.error(exception_context) + }) def get_user_messages(user_id: str = None) -> dict: @@ -306,193 +379,9 @@ def get_user_messages(user_id: str = None) -> dict: return {'queue_list': queue_string, 'processed_list': processed_string, 'queue_count': len(queue), 'processed_count': len(processed)} -def message_parser(message: telegram.telegram_types.Message = None) -> dict: - """ - Parses the message containing the Instagram post link and returns the data. - - Args: - message (telegram.telegram_types.Message): The message object containing the post link. - - Returns: - dict: The data containing the user id, post url, post id, post owner, link type, message id, and chat id. - """ - data = {} - post_id = None - post_owner = None - if re.match(r'^https://www.instagram.com/(p|reel)/.*', message.text): - post_id = message.text.split('/')[4] - post_owner = 'undefined' - elif re.match(r'^https://www.instagram.com/.*/(p|reel)/.*', message.text): - post_id = message.text.split('/')[5] - post_owner = message.text.split('/')[3] - else: - log.error('[Bot]: post link %s from user %s is incorrect', message.text, message.chat.id) - telegram.send_styled_message( - chat_id=message.chat.id, - messages_template={'alias': 'url_error'} - ) - - if post_id: - if len(post_id) == 11 and re.match(r'^[ั‚ะตa-zA-Z0-9_-]+$', post_id): - data['user_id'] = message.chat.id - data['post_url'] = message.text - data['post_id'] = post_id - data['post_owner'] = post_owner - data['link_type'] = 'post' - data['message_id'] = message.id - data['chat_id'] = message.chat.id - else: - log.error('[Bot]: post id %s from user %s is wrong', post_id, message.chat.id) - telegram.send_styled_message( - chat_id=message.chat.id, - messages_template={'alias': 'url_error', 'kwargs': {'url': message.text}} - ) - return data -# END BLOCK ADDITIONAL FUNCTIONS ###################################################################################################### - - -# START BLOCK PROCESSING FUNCTIONS #################################################################################################### -def process_one_post( - message: telegram.telegram_types.Message = None, - help_message: telegram.telegram_types.Message = None, - mode: str = 'single' -) -> None: - """ - Processes an Instagram post link sent by a user and adds it to the queue for download. - - Notice: This method will merge with the `process_posts` method in v3.3.0. - After combining the two buttons into a `Posts` button in version 3.2.0, it makes no sense to split one functionality into two methods. - - Args: - message (telegram.telegram_types.Message): The Telegram message object containing the post link. - help_message (telegram.telegram_types.Message): The help message to be deleted. - mode (str, optional): The mode of processing. Defaults to 'single'. - - Returns: - None - """ - requestor = { - 'user_id': message.chat.id, 'role_id': ROLES_MAP['Posts'], - 'chat_id': message.chat.id, 'message_id': message.message_id - } - user = users_rl.user_access_check(**requestor) - if user.get('permissions', None) == users_rl.user_status_allow: - data = message_parser(message) - if not data: - log.error('[Bot]: link %s cannot be processed', message.text) - else: - rate_limit = user.get('rate_limits', None) - - # Define time to process the message in queue - if rate_limit: - data['scheduled_time'] = rate_limit - else: - data['scheduled_time'] = datetime.now() - - # Check if the message is unique - if database.check_message_uniqueness(data['post_id'], data['user_id']): - status = database.add_message_to_queue(data) - log.info('[Bot]: %s from user %s', status, message.chat.id) - else: - log.info('[Bot]: post %s from user %s already exist in the database', data['post_id'], message.chat.id) - - # If it is not a list of posts - delete users message - if mode == 'single': - telegram.delete_message(message.chat.id, message.id) - if help_message is not None: - telegram.delete_message(message.chat.id, help_message.id) - - -def process_posts( - message: telegram.telegram_types.Message = None, - help_message: telegram.telegram_types.Message = None -) -> None: - """ - Process a single or multiple posts from the user's message. - - Args: - message (telegram.telegram_types.Message, optional): The message containing the list of post links. Defaults to None. - help_message (telegram.telegram_types.Message, optional): The help message to be deleted. Defaults to None. - - Returns: - None - """ - requestor = { - 'user_id': message.chat.id, 'role_id': ROLES_MAP['Posts'], - 'chat_id': message.chat.id, 'message_id': message.message_id - } - user = users.user_access_check(**requestor) - if user.get('permissions', None) == users.user_status_allow: - for link in message.text.split('\n'): - message.text = link - process_one_post( - message=message, - help_message=help_message, - mode='list' - ) - telegram.delete_message(message.chat.id, message.id) - if help_message is not None: - telegram.delete_message(message.chat.id, help_message.id) - - -def reschedule_queue( - message: telegram.telegram_types.Message = None, - help_message: telegram.telegram_types.Message = None -) -> None: - """ - Manually reschedules the queue for the user. - - Args: - message (telegram.telegram_types.Message, optional): The message containing the list of post links. Defaults to None. - help_message (telegram.telegram_types.Message, optional): The help message to be deleted. Defaults to None. - - Returns: - None - """ - requestor = { - 'user_id': message.chat.id, 'role_id': ROLES_MAP['Reschedule Queue'], - 'chat_id': message.chat.id, 'message_id': message.message_id - } - user = users.user_access_check(**requestor) - can_be_deleted = True - if user.get('permissions', None) == users.user_status_allow: - for item in message.text.split('\n'): - item = item.split(': scheduled for ') - post_id = item[0].strip() - new_scheduled_time = datetime.strptime(item[1].strip(), '%Y-%m-%d %H:%M:%S.%f') - if ( - isinstance(post_id, str) and len(post_id) == 11 and - isinstance(new_scheduled_time, datetime) and new_scheduled_time > datetime.now() - ): - database.update_schedule_time_in_queue( - post_id=post_id, - user_id=message.chat.id, - scheduled_time=new_scheduled_time - ) - else: - can_be_deleted = False - telegram.send_styled_message( - chat_id=message.chat.id, - messages_template={'alias': 'wrong_reschedule_queue', 'kwargs': {'current_time': datetime.now()}} - ) - if can_be_deleted: - telegram.delete_message(message.chat.id, message.id) - if help_message is not None: - telegram.delete_message(message.chat.id, help_message.id) -# END BLOCK PROCESSING FUNCTIONS #################################################################################################### - - -# SPECIFIED THREADS ############################################################################################################### +# Threads ########################################################################################################################### def status_message_updater_thread() -> None: - """ - Handler thread for monitoring and timely updating of the widget with the status of messages sent by the user. - - Args: - None - - Returns: - None - """ + """Handler thread for monitoring and timely updating of the widget with the status of messages sent by the user""" log.info('[Message-updater-thread]: started thread for "status_message" updater') while True: time.sleep(STATUSES_MESSAGE_FREQUENCY) @@ -514,15 +403,7 @@ def status_message_updater_thread() -> None: def queue_handler_thread() -> None: - """ - Handler thread to process messages from the queue at the specified time. - - Args: - None - - Returns: - None - """ + """Handler thread to process messages from the queue at the specified time""" log.info('[Queue-handler-thread]: started thread for queue handler') while True: @@ -601,13 +482,11 @@ def queue_handler_thread() -> None: ) else: log.info("[Queue-handler-thread] no messages in the queue for processing at the moment, waiting...") -# SPECIFIED THREADS ############################################################################################################### +# Main ############################################################################################################################# def main(): - """ - The main entry point of the project. - """ + """The main entry point of the project""" # Thread for processing queue thread_queue_handler = threading.Thread(target=queue_handler_thread, args=(), name="QueueHandlerThread") thread_queue_handler.start() @@ -621,7 +500,7 @@ def main(): # Run bot while True: try: - telegram.launch_bot() + tg.launch_bot() except TelegramExceptions.FailedToCreateInstance as telegram_api_exception: log.error('[Bot]: main thread failed, restart thread: %s', telegram_api_exception) time.sleep(5) diff --git a/src/configs/constants.py b/src/configs/constants.py index 2d9dd4e0d..1deeef61e 100644 --- a/src/configs/constants.py +++ b/src/configs/constants.py @@ -10,6 +10,7 @@ # 'button_title': 'role' ROLES_MAP = { 'Posts': 'posts', + 'Account': 'account', 'Reschedule Queue': 'reschedule_queue', } diff --git a/src/configs/databases.json b/src/configs/databases.json index d9f6e4a04..c86214685 100644 --- a/src/configs/databases.json +++ b/src/configs/databases.json @@ -37,6 +37,21 @@ "state VARCHAR(255) NOT NULL DEFAULT 'processed'" ] }, + { + "name": "accounts", + "description": "The table stores the instagram account public information", + "columns": [ + "id SERIAL PRIMARY KEY, ", + "username VARCHAR(50) UNIQUE NOT NULL, ", + "pk NUMERIC NOT NULL, ", + "full_name VARCHAR(255) NOT NULL, ", + "media_count INTEGER NOT NULL, ", + "follower_count INTEGER NOT NULL, ", + "following_count INTEGER NOT NULL, ", + "cursor VARCHAR(255), ", + "last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP" + ] + }, { "name": "migrations", "description": "Table to store the migration history of the database", diff --git a/src/configs/messages.json b/src/configs/messages.json index d0ba95925..70772f5a9 100644 --- a/src/configs/messages.json +++ b/src/configs/messages.json @@ -20,6 +20,10 @@ "text": "{0} To get a backup copy of the list of posts, send links to posts in a list (each new link with a new message line). This message will be automatically split into a number of messages equal to the number of links and processed in the order of the queue.\n {1} Example:\nhttps://www.instagram.com/p/QwEr_tY1234\nhttps://www.instagram.com/p/QwEr_tY1235\nhttps://www.instagram.com/p/QwEr_tY1236", "args": [":information:", ":link:"] }, + "help_for_account": { + "text": "{0} To get a backup copy of the list of posts from the account, send a link to the account. The bot will automatically collect all posts from the account and process them in the order of the queue.\n {1} Example:\nhttps://www.instagram.com/username", + "args": [":information:", ":link:"] + }, "help_for_reschedule_queue": { "text": "{0} To reschedule the processing of messages in the queue, simply send a list of messages with a modified processing time.\nfor example:\nq1wRty12345: scheduled for 2021-12-31 23:59:59\nq1wRty12346: scheduled for 2021-12-31 23:59:59\nq1wRty12347: scheduled for 2021-12-31 23:59:59", "args": [":information:"] diff --git a/src/modules/database.py b/src/modules/database.py index 85474bd41..a83849d94 100644 --- a/src/modules/database.py +++ b/src/modules/database.py @@ -65,16 +65,14 @@ class DatabaseClient: check_message_uniqueness(post_id, user_id): Check if a message with the given post ID and chat ID already exists in the queue. keep_message(message_id, chat_id, message_content, **kwargs): Add a message to the messages table in the database. get_users(only_allowed): Get a list of users from the users table in the database. - get_considered_message(message_type, chat_id): Get a message with specified type and + get_considered_message(message_type, chat_id): Get a message with specified type and chat ID from the messages table in the database. + add_account_info(data): Add account information to the accounts table in the database. + get_account_info(account_name): Get the account information from the accounts table in the database. Rises: psycopg2.Error: An error occurred while interacting with the PostgreSQL database. """ - def __init__( - self, - vault: object = None, - db_role: str = None - ) -> None: + def __init__(self, vault: object = None, db_role: str = None) -> None: """ Initializes a new instance of the Database client. @@ -122,15 +120,11 @@ def create_connection_pool(self) -> pool.SimpleConnectionPool: '[Database]: Creating a connection pool for the %s:%s/%s', db_configuration['host'], db_configuration['port'], db_configuration['dbname'] ) - return pool.SimpleConnectionPool( - minconn=1, - maxconn=db_configuration['connections'], - host=db_configuration['host'], - port=db_configuration['port'], - user=db_credentials['username'], - password=db_credentials['password'], - database=db_configuration['dbname'] - ) + settings = { + 'minconn': 1, 'maxconn': db_configuration['connections'], 'host': db_configuration['host'], 'port': db_configuration['port'], + 'user': db_credentials['username'], 'password': db_credentials['password'], 'database': db_configuration['dbname'] + } + return pool.SimpleConnectionPool(**settings) def get_connection(self) -> psycopg2.extensions.connection: """ @@ -161,10 +155,7 @@ def _prepare_db(self) -> None: # Create databases if does not exist for table in database_init_configuration.get('Tables', None): - self._create_table( - table_name=table['name'], - columns="".join(f"{column}" for column in table['columns']) - ) + self._create_table(table_name=table['name'], columns="".join(f"{column}" for column in table['columns'])) log.info('[Database]: Prepare Database: create table `%s` (if does not exist)', table['name']) # Write necessary data to the database (service records) @@ -172,11 +163,7 @@ def _prepare_db(self) -> None: # ! This code block needs to be improved after some service data will appear for filling, # ! because this code creates duplicate lines each time the project is started. for data in database_init_configuration['DataSeeding']: - self._insert( - table_name=data['table'], - columns=tuple(data['data'].keys()), - values=tuple(data['data'].values()) - ) + self._insert(table_name=data['table'], columns=tuple(data['data'].keys()), values=tuple(data['data'].values())) log.info('[Database]: Prepare Database: data seeding has been added to the `%s` table', data['table']) def _migrations(self) -> None: @@ -205,10 +192,7 @@ def _migrations(self) -> None: else: log.error('[Database]: Migrations: the %s is not a valid migration file', migration_file) - def _is_migration_executed( - self, - migration_name: str = None - ) -> bool: + def _is_migration_executed(self, migration_name: str = None) -> bool: """ Check if a migration has already been executed. @@ -220,11 +204,7 @@ def _is_migration_executed( """ return self._select(table_name='migrations', columns=('id',), condition=f"name = '{migration_name}'") - def _mark_migration_as_executed( - self, - migration_name: str = None, - version: str = None - ) -> None: + def _mark_migration_as_executed(self, migration_name: str = None, version: str = None) -> None: """ Inserts a migration into the migrations table to mark it as executed. @@ -233,11 +213,7 @@ def _mark_migration_as_executed( """ self._insert(table_name='migrations', columns=('name', 'version'), values=(migration_name, version)) - def _create_table( - self, - table_name: str = None, - columns: str = None - ) -> None: + def _create_table(self, table_name: str = None, columns: str = None) -> None: """ Create a new table in the database with the given name and columns if it does not already exist. @@ -256,12 +232,7 @@ def _create_table( self.close_connection(conn) @reconnect_on_exception - def _insert( - self, - table_name: str = None, - columns: tuple = None, - values: tuple = None - ) -> None: + def _insert(self, table_name: str = None, columns: tuple = None, values: tuple = None) -> None: """ Inserts a new row into the specified table with the given columns and values. @@ -284,19 +255,24 @@ def _insert( cursor.execute(sql_query, values) conn.commit() self.close_connection(conn) - except (psycopg2.Error, IndexError) as error: + except IndexError as error: log.error( '[Database]: An error occurred while inserting a row into the table %s: %s\nColumns: %s\nValues: %s\nQuery: %s', table_name, error, columns, values, sql_query ) + except TypeError as error: + log.error( + '[Database]: Wrong data type in the columns or values: %s\nColumns: %s\nValues: %s\nQuery: %s', + error, columns, values, sql_query + ) + except psycopg2.Error as error: + log.error( + '[Database]: A database-related error occurred: %s\nColumns: %s\nValues: %s\nQuery: %s', + error, columns, values, sql_query + ) @reconnect_on_exception - def _select( - self, - table_name: str = None, - columns: tuple = None, - **kwargs - ) -> list | None: + def _select(self, table_name: str = None, columns: tuple = None, **kwargs) -> list | None: """ Selects rows from the specified table with the given columns based on the specified condition. @@ -336,12 +312,7 @@ def _select( return response if response else None @reconnect_on_exception - def _update( - self, - table_name: str = None, - values: str = None, - condition: str = None - ) -> None: + def _update(self, table_name: str = None, values: str = None, condition: str = None) -> None: """ Update the specified table with the given values of values based on the specified condition. @@ -360,11 +331,7 @@ def _update( self.close_connection(conn) @reconnect_on_exception - def _delete( - self, - table_name: str = None, - condition: str = None - ) -> None: + def _delete(self, table_name: str = None, condition: str = None) -> None: """ Delete rows from a table based on a condition. @@ -389,27 +356,16 @@ def _reset_stale_records(self) -> None: """ # Reset stale status_message (can be only one status_message per chat) log.info('[Database]: Resetting stale status messages...') - status_messages = self._select( - table_name='messages', - columns=("id", "state"), - condition="message_type = 'status_message'", - ) + status_messages = self._select(table_name='messages', columns=("id", "state"), condition="message_type = 'status_message'") if status_messages: for message in status_messages: if message[1] != 'updated': - self._update( - table_name='messages', - values="state = 'updated'", - condition=f"id = '{message[0]}'" - ) + self._update(table_name='messages', values="state = 'updated'", condition=f"id = '{message[0]}'") log.info('[Database]: Stale status messages have been reset') else: log.info('[Database]: No stale status messages found') - def add_message_to_queue( - self, - data: dict = None - ) -> str: + def add_message_to_queue(self, data: dict = None) -> str: """ Add a message to the queue table in the database. @@ -450,36 +406,18 @@ def add_message_to_queue( self._insert( table_name='queue', columns=( - "user_id", - "post_id", - "post_url", - "post_owner", - "link_type", - "message_id", - "chat_id", - "scheduled_time", - "download_status", - "upload_status" + "user_id", "post_id", "post_url", "post_owner", "link_type", + "message_id", "chat_id", "scheduled_time", "download_status", "upload_status" ), values=( - data.get('user_id', None), - data.get('post_id', None), - data.get('post_url', None), - data.get('post_owner', None), - data.get('link_type', None), - data.get('message_id', None), - data.get('chat_id', None), - data.get('scheduled_time', None), - data.get('download_status', 'not started'), - data.get('upload_status', 'not started'), + data.get('user_id', None), data.get('post_id', None), data.get('post_url', None), data.get('post_owner', None), + data.get('link_type', None), data.get('message_id', None), data.get('chat_id', None), data.get('scheduled_time', None), + data.get('download_status', 'not started'), data.get('upload_status', 'not started') ) ) return f"{data.get('message_id', None)}: added to queue" - def get_message_from_queue( - self, - scheduled_time: str = None - ) -> tuple: + def get_message_from_queue(self, scheduled_time: str = None) -> tuple: """ Get a one message from the queue table that is scheduled to be sent at the specified time. The message will be returned before or equal to the specified timestamp in the argument. @@ -496,19 +434,11 @@ def get_message_from_queue( datetime.datetime(2023, 11, 14, 21, 21, 22, 603440), 'None', 'None', datetime.datetime(2023, 11, 14, 21, 14, 26, 680024), 'waiting') """ message = self._select( - table_name='queue', - columns=("*",), - condition=f"scheduled_time <= '{scheduled_time}' AND state IN ('waiting', 'processing')", - limit=1 + table_name='queue', columns=("*"), condition=f"scheduled_time <= '{scheduled_time}' AND state IN ('waiting', 'processing')", limit=1 ) return message[0] if message else None - def update_message_state_in_queue( - self, - post_id: str = None, - state: str = None, - **kwargs - ) -> str: + def update_message_state_in_queue(self, post_id: str = None, state: str = None, **kwargs) -> str: """ Update the state of a message in the queue table and move it to the processed table if the state is 'processed'. @@ -551,37 +481,16 @@ def update_message_state_in_queue( self._update(table_name='queue', values=values, condition=f"post_id = '{post_id}'") if state == 'processed': - processed_message = self._select( - table_name='queue', - columns=("*",), - condition=f"post_id = '{post_id}'", - limit=1 - ) + processed_message = self._select(table_name='queue', columns=("*"), condition=f"post_id = '{post_id}'", limit=1) self._insert( table_name='processed', columns=( - "user_id", - "post_id", - "post_url", - "post_owner", - "link_type", - "message_id", - "chat_id", - "download_status", - "upload_status", - "state" + "user_id", "post_id", "post_url", "post_owner", "link_type", "message_id", "chat_id", "download_status", "upload_status", "state" ), values=( - processed_message[0][1], - processed_message[0][2], - processed_message[0][3], - processed_message[0][4], - processed_message[0][5], - processed_message[0][6], - processed_message[0][7], - kwargs.get('download_status', 'pending'), - kwargs.get('upload_status', 'pending'), - state + processed_message[0][1], processed_message[0][2], processed_message[0][3], processed_message[0][4], + processed_message[0][5], processed_message[0][6], processed_message[0][7], + kwargs.get('download_status', 'pending'), kwargs.get('upload_status', 'pending'), state ) ) self._delete(table_name='queue', condition=f"post_id = '{post_id}'") @@ -591,12 +500,7 @@ def update_message_state_in_queue( return response - def update_schedule_time_in_queue( - self, - post_id: str = None, - user_id: str = None, - scheduled_time: str = None - ) -> str: + def update_schedule_time_in_queue(self, post_id: str = None, user_id: str = None, scheduled_time: str = None) -> str: """ Update the scheduled time of a message in the queue table. @@ -612,17 +516,10 @@ def update_schedule_time_in_queue( >>> update_schedule_time_in_queue(post_id='123', user_id='12345', scheduled_time='2022-01-01 12:00:00') '123: scheduled time updated' """ - self._update( - table_name='queue', - values=f"scheduled_time = '{scheduled_time}'", - condition=f"post_id = '{post_id}' AND user_id = '{user_id}'" - ) + self._update(table_name='queue', values=f"scheduled_time = '{scheduled_time}'", condition=f"post_id = '{post_id}' AND user_id = '{user_id}'") return f"{post_id}: scheduled time updated" - def get_user_queue( - self, - user_id: str = None - ) -> dict: + def get_user_queue(self, user_id: str = None) -> dict: """ Get messages from the queue table for the specified user. @@ -638,21 +535,14 @@ def get_user_queue( """ result = [] queue = self._select( - table_name='queue', - columns=("post_id", "scheduled_time"), - condition=f"user_id = '{user_id}'", - order_by='scheduled_time ASC', - limit=10000 + table_name='queue', columns=("post_id", "scheduled_time"), condition=f"user_id = '{user_id}'", order_by='scheduled_time ASC', limit=10000 ) if queue: for message in queue: result.append({'post_id': message[0], 'scheduled_time': message[1]}) return result - def get_user_processed( - self, - user_id: str = None - ) -> dict: + def get_user_processed(self, user_id: str = None) -> dict: """ Get last ten messages from the processed table for the specified user. It is used to display the last messages sent by the bot to the user. @@ -669,22 +559,15 @@ def get_user_processed( """ result = [] processed = self._select( - table_name='processed', - columns=("post_id", "timestamp", "state"), - condition=f"user_id = '{user_id}'", - order_by='timestamp ASC', - limit=10000 + table_name='processed', columns=("post_id", "timestamp", "state"), + condition=f"user_id = '{user_id}'", order_by='timestamp ASC', limit=10000 ) if processed: for message in processed: result.append({'post_id': message[0], 'timestamp': message[1], 'state': message[2]}) return result - def check_message_uniqueness( - self, - post_id: str = None, - user_id: str = None - ) -> bool: + def check_message_uniqueness(self, post_id: str = None, user_id: str = None) -> bool: """ Check if a message with the given post ID and chat ID already exists in the queue. @@ -699,29 +582,13 @@ def check_message_uniqueness( >>> check_message_uniqueness(post_id='12345', user_id='67890') True """ - queue = self._select( - table_name='queue', - columns=("id",), - condition=f"post_id = '{post_id}' AND user_id = '{user_id}'", - limit=1 - ) - processed = self._select( - table_name='processed', - columns=("id",), - condition=f"post_id = '{post_id}' AND user_id = '{user_id}'", - limit=1 - ) + queue = self._select(table_name='queue', columns=("id",), condition=f"post_id = '{post_id}' AND user_id = '{user_id}'", limit=1) + processed = self._select(table_name='processed', columns=("id",), condition=f"post_id = '{post_id}' AND user_id = '{user_id}'", limit=1) if queue or processed: return False return True - def keep_message( - self, - message_id: str = None, - chat_id: str = None, - message_content: str | dict = None, - **kwargs - ) -> str: + def keep_message(self, message_id: str = None, chat_id: str = None, message_content: str | dict = None, **kwargs) -> str: """ Add a message to the messages table in the database. It is used to store the last message sent to the user for updating the message in the future. @@ -748,9 +615,7 @@ def keep_message( recreated = kwargs.get('recreated', False) message_content_hash = get_hash(message_content) check_exist_message_type = self._select( - table_name='messages', - columns=("id", "message_id"), - condition=f"message_type = '{message_type}' AND chat_id = '{chat_id}'", + table_name='messages', columns=("id", "message_id"), condition=f"message_type = '{message_type}' AND chat_id = '{chat_id}'" ) response = None @@ -758,11 +623,8 @@ def keep_message( self._update( table_name='messages', values=( - f"message_content_hash = '{message_content_hash}', " - f"message_id = '{message_id}', " - f"state = '{state}', " - "updated_at = CURRENT_TIMESTAMP, " - "created_at = CURRENT_TIMESTAMP" + f"message_content_hash = '{message_content_hash}', message_id = '{message_id}', " + f"state = '{state}', updated_at = CURRENT_TIMESTAMP, created_at = CURRENT_TIMESTAMP" ), condition=f"id = '{check_exist_message_type[0][0]}'" ) @@ -772,10 +634,7 @@ def keep_message( self._update( table_name='messages', values=( - f"message_content_hash = '{message_content_hash}', " - f"message_id = '{message_id}', " - f"state = '{state}', " - f"updated_at = CURRENT_TIMESTAMP" + f"message_content_hash = '{message_content_hash}', message_id = '{message_id}', state = '{state}', updated_at = CURRENT_TIMESTAMP" ), condition=f"id = '{check_exist_message_type[0][0]}'" ) @@ -795,10 +654,7 @@ def keep_message( return response - def get_users( - self, - only_allowed: bool = True - ) -> dict: + def get_users(self, only_allowed: bool = True) -> dict: """ This method will be deprecated after https://github.com/obervinov/users-package/issues/44 (users-package:v3.1.0). Get a dictionary of all users with their metadata from the users table in the database. @@ -816,29 +672,16 @@ def get_users( """ users_dict = [] if only_allowed: - users = self._select( - table_name='users', - columns=("user_id", "chat_id", "status"), - condition="status = 'allowed'", - limit=1000 - ) + users = self._select(table_name='users', columns=("user_id", "chat_id", "status"), condition="status = 'allowed'", limit=1000) else: - users = self._select( - table_name='users', - columns=("user_id", "chat_id", "status"), - limit=1000 - ) + users = self._select(table_name='users', columns=("user_id", "chat_id", "status"), limit=1000) if users: for user in users: users_dict.append({'user_id': user[0], 'chat_id': user[1], 'status': user[2]}) return users_dict - def get_considered_message( - self, - message_type: str = None, - chat_id: str = None - ) -> tuple: + def get_considered_message(self, message_type: str = None, chat_id: str = None) -> tuple: """ Get a message with specified type and chat ID from the messages table in the database. @@ -861,3 +704,47 @@ def get_considered_message( limit=1 ) return message[0] if message else None + + def add_account_info(self, data: dict = None) -> None: + """ + Add account information to the accounts table in the database. + + Args: + data (dict): A dictionary containing the account details. Keep only the necessary keys. + + Parameters: + username (str): The username of the account. + pk (int): The primary key of the account. + full_name (str): The full name of the account. + media_count (int): The number of media items in the account. + follower_count (int): The number of followers of the account. + following_count (int): The number of accounts the account is following. + cursor (str): The cursor for the account. + """ + keys_to_keep = ['username', 'pk', 'full_name', 'media_count', 'follower_count', 'following_count', 'cursor'] + filtered_dict = {key: data[key] for key in keys_to_keep if key in data} + exist_account = self._select(table_name='accounts', columns=("id",), condition=f"username = '{data.get('username')}'") + + if exist_account: + self._update( + table_name='accounts', + values=", ".join(f"{key} = '{value}'" for key, value in filtered_dict.items()), + condition=f"id = '{exist_account[0][0]}'" + ) + else: + self._insert(table_name='accounts', columns=tuple(filtered_dict.keys()), values=tuple(filtered_dict.values())) + + def get_account_info(self, username: str = None) -> tuple: + """ + Get the account information from the accounts table in the database. + + Args: + username (str): The username of the account. + + Returns: + tuple: A tuple containing the account information from the accounts table. + pk (int): The primary key of the account. + cursor (str): The cursor for the account. + """ + account = self._select(table_name='accounts', columns=("pk", "cursor"), condition=f"username = '{username}'", limit=1) + return account[0] if account else (None, None) diff --git a/src/modules/downloader.py b/src/modules/downloader.py index f26f4c0e1..11c4a69a5 100644 --- a/src/modules/downloader.py +++ b/src/modules/downloader.py @@ -28,6 +28,7 @@ class Downloader: :attribute download_methods (dict): dictionary with download methods for instagram api client. :attribute general_settings_list (list): list of general session settings for the instagram api. :attribute device_settings_list (list): list of device settings for the instagram api. + :attribute media_type_links (dict): dictionary with media type links for the instagram api client. Methods: :method _get_login_args: get login arguments for the instagram api. @@ -112,10 +113,10 @@ def __init__( "Please check the configuration in class argument or the secret with the configuration in the Vault." ) - log.info('[Downloader]: Creating a new instance...') + log.info('[Downloader]: creating a new instance...') self.client = Client() - log.info('[Downloader]: Setting up the client configuration...') + log.info('[Downloader]: setting up the client configuration...') self.client.delay_range = [1, int(self.configuration['delay-requests'])] self.client.request_timeout = int(self.configuration['request-timeout']) self.client.set_proxy(dsn=self.configuration.get('proxy-dsn', None)) @@ -126,16 +127,14 @@ def __init__( 'app_version', 'version_code', 'manufacturer', 'model', 'device', 'cpu', 'dpi', 'resolution', 'android_release', 'android_version' ] self.download_methods = { - (1, 'any'): self.client.photo_download, - (2, 'feed'): self.client.video_download, - (2, 'clips'): self.client.clip_download, - (2, 'igtv'): self.client.igtv_download, - (8, 'any'): self.client.album_download + (1, 'any'): self.client.photo_download, (2, 'feed'): self.client.video_download, (2, 'clips'): self.client.clip_download, + (2, 'igtv'): self.client.igtv_download, (8, 'any'): self.client.album_download } + self.media_type_links = {1: 'p', 8: 'p', 2: 'reel'} auth_status = self.login() if auth_status == 'logged_in': - log.info('[Downloader]: Instance created successfully with account %s', self.configuration['username']) + log.info('[Downloader]: instance created successfully with account %s', self.configuration['username']) else: raise FailedAuthInstagram("Failed to authenticate the Instaloader instance.") @@ -143,7 +142,7 @@ def _get_login_args(self) -> dict: """Get login arguments for the Instagram API""" if self.configuration['2fa-enabled']: totp_code = self.client.totp_generate_code(seed=self.configuration['2fa-seed']) - log.info('[Downloader]: Two-factor authentication is enabled. TOTP code: %s', totp_code) + log.info('[Downloader]: 2fa is enabled. TOTP code: %s', totp_code) return { 'username': self.configuration['username'], 'password': self.configuration['password'], @@ -159,11 +158,11 @@ def _create_new_session(self, login_args: dict) -> None: self._set_session_settings() self.client.login(**login_args) self.client.dump_settings(self.configuration['session-file']) - log.info('[Downloader]: The new session file was created successfully: %s', self.configuration['session-file']) + log.info('[Downloader]: the new session file was created successfully: %s', self.configuration['session-file']) def _handle_relogin(self, login_args: dict) -> None: """Handle re-authentication in the Instagram API""" - log.info('[Downloader]: Authentication with the clearing of the session...') + log.info('[Downloader]: authentication with the clearing of the session...') old_uuids = self.client.get_settings().get("uuids", {}) self.client.set_settings({}) self.client.set_uuids(old_uuids) @@ -171,7 +170,7 @@ def _handle_relogin(self, login_args: dict) -> None: def _load_session(self, login_args: dict) -> None: """Load or create a session.""" - log.info('[Downloader]: Authentication with the existing session...') + log.info('[Downloader]: authentication with the existing session...') session_file = self.configuration['session-file'] if os.path.exists(session_file): self.client.load_settings(session_file) @@ -192,18 +191,18 @@ def _set_session_settings(self) -> None: - device settings - user agent """ - log.info('[Downloader]: Extracting device settings...') + log.info('[Downloader]: extracting device settings...') device_settings = json.loads(self.configuration['device-settings']) if not all(item in device_settings.keys() for item in self.device_settings_list): - raise ValueError("Incorrect device settings in the configuration. Please check the configuration in the Vault.") + raise ValueError("incorrect device settings in the configuration. Please check the configuration in the Vault.") # Extract other settings except device settings - log.info('[Downloader]: Extracting other settings...') + log.info('[Downloader]: extracting other settings...') other_settings = {item: None for item in self.general_settings_list} for item in other_settings.keys(): other_settings[item] = self.configuration[item.replace('_', '-')] - log.debug('[Downloader]: Retrieved settings: %s', {**other_settings, 'device_settings': device_settings}) + log.debug('[Downloader]: retrieved settings: %s', {**other_settings, 'device_settings': device_settings}) # Apply all session settings self.client.set_settings(settings={**other_settings, 'device_settings': device_settings}) @@ -211,7 +210,7 @@ def _set_session_settings(self) -> None: # Country in set_settings is not working self.client.set_country(country=other_settings['country']) self.client.set_user_agent() - log.info('[Downloader]: General session settings have been successfully set: %s', self.client.get_settings()) + log.info('[Downloader]: general session settings have been successfully set: %s', self.client.get_settings()) def _validate_session_settings(self) -> bool: """ @@ -220,12 +219,12 @@ def _validate_session_settings(self) -> bool: Returns: (bool) True if the session settings are equal to the configuration settings, otherwise False. """ - log.info('[Downloader]: Checking the difference between the session settings and the configuration settings...') + log.info('[Downloader]: checking the difference between the session settings and the configuration settings...') session_settings = self.client.get_settings() for item in self.general_settings_list: if str(session_settings[item]) != str(self.configuration[item.replace('_', '-')]): log.info( - '[Downloader]: The session key value are not equal to the expected value: %s != %s. Session will be reset', + '[Downloader]: the session key value are not equal to the expected value: %s != %s. Session will be reset', session_settings[item], self.configuration[item.replace('_', '-')] ) return False @@ -233,11 +232,11 @@ def _validate_session_settings(self) -> bool: for item in self.device_settings_list: if str(device_settings[item]) != str(json.loads(self.configuration['device-settings'])[item]): log.info( - '[Downloader]: The session key value are not equal to the expected value: %s != %s. Session will be reset', + '[Downloader]: the session key value are not equal to the expected value: %s != %s. Session will be reset', device_settings[item], json.loads(self.configuration['device-settings'])[item] ) return False - log.info('[Downloader]: The session settings are equal to the expected settings.') + log.info('[Downloader]: the session settings are equal to the expected settings.') return True @staticmethod @@ -253,22 +252,22 @@ def wrapper(self, *args, **kwargs): try: return method(self, *args, **kwargs) except LoginRequired: - log.error('[Downloader]: Instagram API login required. Re-authenticate after %s minutes', round(random_shift/60)) + log.error('[Downloader]: instagram API login required. Re-authenticate after %s minutes', round(random_shift/60)) time.sleep(random_shift) - log.info('[Downloader]: Re-authenticate after timeout due to login required') + log.info('[Downloader]: re-authenticate after timeout due to login required') self.login(method='relogin') except ChallengeRequired: - log.error('[Downloader]: Instagram API requires challenge in browser. Retry after %s minutes', round(random_shift/60)) + log.error('[Downloader]: instagram API requires challenge in browser. Retry after %s minutes', round(random_shift/60)) time.sleep(random_shift) - log.info('[Downloader]: Re-authenticate after timeout due to challenge required') + log.info('[Downloader]: re-authenticate after timeout due to challenge required') self.login() except PleaseWaitFewMinutes: - log.error('[Downloader]: Device or IP address has been restricted. Just wait a %s minutes and retry', round(random_shift/60)) + log.error('[Downloader]: device or IP address has been restricted. Just wait a %s minutes and retry', round(random_shift/60)) time.sleep(random_shift) - log.info('[Downloader]: Retry after timeout due to restriction') + log.info('[Downloader]: retry after timeout due to restriction') self.login(method='relogin') except (ReadTimeoutError, RequestsConnectionError, ClientRequestTimeout): - log.error('[Downloader]: Timeout error downloading post content. Retry after 1 minute') + log.error('[Downloader]: timeout error downloading post content. Retry after 1 minute') time.sleep(60) return method(self, *args, **kwargs) return wrapper @@ -289,7 +288,7 @@ def login(self, method: str = 'session') -> str | None: or None """ - log.info('[Downloader]: Authentication in the Instagram API with type: %s', method) + log.info('[Downloader]: authentication in the Instagram API with type: %s', method) # Generate login arguments login_args = self._get_login_args() @@ -301,9 +300,9 @@ def login(self, method: str = 'session') -> str | None: self._load_session(login_args) # Check the status of the authentication - log.info('[Downloader]: Checking the status of the authentication...') + log.info('[Downloader]: checking the status of the authentication...') self.client.get_timeline_feed() - log.info('[Downloader]: Authentication in the Instagram API was successful.') + log.info('[Downloader]: authentication in the Instagram API was successful.') return 'logged_in' @@ -325,10 +324,10 @@ def get_post_content(self, shortcode: str = None, error_count: int = 0) -> dict } """ if error_count > 3: - log.error('[Downloader]: The number of errors exceeded the limit: %s', error_count) - raise FailedDownloadPost("The number of errors exceeded the limit.") + log.error('[Downloader]: the number of errors exceeded the limit: %s', error_count) + raise FailedDownloadPost("the number of errors exceeded the limit.") - log.info('[Downloader]: Downloading the contents of the post %s...', shortcode) + log.info('[Downloader]: downloading the contents of the post %s...', shortcode) try: media_pk = self.client.media_pk_from_code(code=shortcode) media_info = self.client.media_info(media_pk=media_pk).dict() @@ -345,11 +344,11 @@ def get_post_content(self, shortcode: str = None, error_count: int = 0) -> dict download_method(media_pk=media_pk, folder=path) status = "completed" else: - log.error('[Downloader]: The media type is not supported for download: %s', media_info) + log.error('[Downloader]: the media type is not supported for download: %s', media_info) status = "not_supported" if os.listdir(path): - log.info('[Downloader]: The contents of the post %s have been successfully downloaded', shortcode) + log.info('[Downloader]: the contents of the post %s have been successfully downloaded', shortcode) response = { 'post': shortcode, 'owner': media_info['user']['username'], @@ -357,7 +356,7 @@ def get_post_content(self, shortcode: str = None, error_count: int = 0) -> dict 'status': status if status else 'completed' } else: - log.error('[Downloader]: Temporary directory is empty: %s', path) + log.error('[Downloader]: temporary directory is empty: %s', path) response = { 'post': shortcode, 'owner': media_info['user']['username'], @@ -366,7 +365,7 @@ def get_post_content(self, shortcode: str = None, error_count: int = 0) -> dict } except (MediaUnavailable, MediaNotFound): - log.warning('[Downloader]: Post %s not found, perhaps it was deleted. Message will be marked as processed', shortcode) + log.warning('[Downloader]: post %s not found, perhaps it was deleted. Message will be marked as processed', shortcode) response = { 'post': shortcode, 'owner': 'undefined', @@ -375,3 +374,32 @@ def get_post_content(self, shortcode: str = None, error_count: int = 0) -> dict } return response + + @exceptions_handler + def get_account_info(self, username: str = None) -> dict | None: + """ + The method for getting information about the account by the specified User ID. + + Args: + :param username (str): the ID of the user for downloading content. + + Returns: + (dict) account information + """ + log.info('[Downloader]: extracting information about the account %s...', username) + return self.client.user_info_by_username(username=username).dict() + + @exceptions_handler + def get_account_posts(self, user_id: int = None, cursor: str = None) -> list | None: + """ + The method for getting the content of a post from a specified User ID. + + Args: + :param user_id (int): the ID of the user for downloading content. + :param cursor (str): the cursor for pagination. + + Returns: + (list) list of posts + """ + log.info('[Downloader]: extracting the list of posts for the user pk %s...', user_id) + return self.client.user_medias_paginated(user_id=user_id, amount=6, end_cursor=cursor) diff --git a/src/modules/uploader.py b/src/modules/uploader.py index 6cba9550f..50d8f9753 100644 --- a/src/modules/uploader.py +++ b/src/modules/uploader.py @@ -88,10 +88,7 @@ def run_transfers( log.info('[Uploader]: Preparing media files for transfer to the cloud...') for root, _, files in os.walk(f"{self.configuration['source-directory']}{sub_directory}"): for file in files: - transfers[file] = self.upload_to_cloud( - source=os.path.join(root, file), - destination=root.split('/')[1] - ) + transfers[file] = self.upload_to_cloud(source=os.path.join(root, file), destination=root.split('/')[1]) if transfers[file] == 'uploaded': os.remove(os.path.join(root, file)) result = 'completed' diff --git a/tests/conftest.py b/tests/conftest.py index 85f4a2650..637a71716 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -449,7 +449,6 @@ def fixture_postgres_queue_test_data(postgres_instance): { 'user_id': 'test_user_1', 'post_id': 'test_post_1', - 'post_url': 'https://example.com/p/test_post_1', 'post_owner': 'test_owner_1', 'link_type': 'post', 'message_id': 'test_message_1', @@ -462,7 +461,6 @@ def fixture_postgres_queue_test_data(postgres_instance): { 'user_id': 'test_user_2', 'post_id': 'test_post_2', - 'post_url': 'https://example.com/p/test_post_2', 'post_owner': 'test_owner_2', 'link_type': 'post', 'message_id': 'test_message_2', @@ -475,7 +473,6 @@ def fixture_postgres_queue_test_data(postgres_instance): { 'user_id': 'test_user_3', 'post_id': 'test_post_3', - 'post_url': 'https://example.com/p/test_post_3', 'post_owner': 'test_owner_3', 'link_type': 'post', 'message_id': 'test_message_3', @@ -493,9 +490,9 @@ def fixture_postgres_queue_test_data(postgres_instance): "(user_id, post_id, post_url, post_owner, link_type, message_id, chat_id, scheduled_time, download_status, upload_status, state) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", ( - message['user_id'], message['post_id'], message['post_url'], message['post_owner'], message['link_type'], - message['message_id'], message['chat_id'], message['scheduled_time'], message['download_status'], - message['upload_status'], message['state'] + message['user_id'], message['post_id'], f"https://www.instagram.com/p/{message['post_id']}", + message['post_owner'], message['link_type'], message['message_id'], message['chat_id'], + message['scheduled_time'], message['download_status'], message['upload_status'], message['state'] ) ) conn.commit() @@ -510,7 +507,6 @@ def fixture_postgres_processed_test_data(postgres_instance): { 'user_id': 'test_user_4', 'post_id': 'test_post_4', - 'post_url': 'https://example.com/p/test_post_4', 'post_owner': 'test_owner_4', 'link_type': 'post', 'message_id': 'test_message_4', @@ -522,7 +518,6 @@ def fixture_postgres_processed_test_data(postgres_instance): { 'user_id': 'test_user_5', 'post_id': 'test_post_5', - 'post_url': 'https://example.com/p/test_post_5', 'post_owner': 'test_owner_5', 'link_type': 'post', 'message_id': 'test_message_5', @@ -534,7 +529,6 @@ def fixture_postgres_processed_test_data(postgres_instance): { 'user_id': 'test_user_6', 'post_id': 'test_post_6', - 'post_url': 'https://example.com/p/test_post_6', 'post_owner': 'test_owner_6', 'link_type': 'post', 'message_id': 'test_message_6', @@ -550,8 +544,8 @@ def fixture_postgres_processed_test_data(postgres_instance): "INSERT INTO processed (user_id, post_id, post_url, post_owner, link_type, message_id, chat_id, download_status, upload_status, state) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", ( - message['user_id'], message['post_id'], message['post_url'], message['post_owner'], message['link_type'], - message['message_id'], message['chat_id'], message['download_status'], message['upload_status'], + message['user_id'], message['post_id'], f"https://www.instagram.com/p/{message['post_id']}", message['post_owner'], + message['link_type'], message['message_id'], message['chat_id'], message['download_status'], message['upload_status'], message['state'] ) ) diff --git a/tests/postgres/tables.sql b/tests/postgres/tables.sql index d76463748..ab74115d0 100644 --- a/tests/postgres/tables.sql +++ b/tests/postgres/tables.sql @@ -51,6 +51,19 @@ CREATE TABLE processed ( state VARCHAR (50) NOT NULL DEFAULT 'processed' ); +-- Schema for the accounts table +CREATE TABLE accounts ( + id serial PRIMARY KEY, + username VARCHAR (50) UNIQUE NOT NULL, + pk NUMERIC NOT NULL, + full_name VARCHAR (255) NOT NULL, + media_count INTEGER NOT NULL, + follower_count INTEGER NOT NULL, + following_count INTEGER NOT NULL, + cursor VARCHAR (255), + last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + -- Schema for the migrations table CREATE TABLE migrations ( id serial PRIMARY KEY, diff --git a/tests/test_database.py b/tests/test_database.py index e365af23b..ce5a04c3a 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -98,7 +98,7 @@ def test_messages_queue(database_class): data = { 'user_id': 'test_case_6', 'post_id': 'test_case_6', - 'post_url': 'https://example.com/p/test_case_6', + 'post_url': 'https://www.instagram.com/p/test_case_6', 'post_owner': 'test_case_6', 'link_type': 'post', 'message_id': 'test_case_6',