diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..e9d7acf
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..37985fd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+venv
+*pycache*
+*.xls*
+*.log
+*.db
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/dj_bot.conf b/dj_bot.conf
new file mode 100644
index 0000000..f9fb6ff
--- /dev/null
+++ b/dj_bot.conf
@@ -0,0 +1,12 @@
+# supervisorctl
+[program:dj_bot]
+directory=/root/djimbo_template/
+command=python3.9 main.py
+
+autostart=True
+autorestart=True
+
+stderr_logfile=/root/djimbo_template/tgbot/data/sv_log_err.log
+; stderr_logfile_maxbytes=10MB
+stdout_logfile=/root/djimbo_template/tgbot/data/sv_log_out.log
+; stdout_logfile_maxbytes=10MB
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..114df9d
--- /dev/null
+++ b/main.py
@@ -0,0 +1,83 @@
+# - *- coding: utf- 8 - *-
+import asyncio
+import os
+import sys
+
+import colorama
+from aiogram import Bot, Dispatcher
+from aiogram.client.default import DefaultBotProperties
+
+from tgbot.data.config import BOT_TOKEN, BOT_SCHEDULER, get_admins
+from tgbot.database.db_helper import create_dbx
+from tgbot.middlewares import register_all_middlwares
+from tgbot.routers import register_all_routers
+from tgbot.services.api_session import AsyncRequestSession
+from tgbot.utils.misc.bot_commands import set_commands
+from tgbot.utils.misc.bot_logging import bot_logger
+from tgbot.utils.misc_functions import autobackup_admin, startup_notify, tenders_sched, tenders_sched_ap, tenders_sched_ap_in_tenderplan
+
+colorama.init()
+
+
+# Запуск шедулеров
+async def scheduler_start(bot):
+ # BOT_SCHEDULER.add_job(autobackup_admin, trigger="cron", hour=00, args=(bot,)) # Ежедневный Автобэкап в 00:00
+ BOT_SCHEDULER.add_job(tenders_sched_ap_in_tenderplan, 'cron', hour='21', minute='0', args=(bot,)) #
+ # BOT_SCHEDULER.add_job(tenders_sched, 'cron', hour='18', minute='0', args=(bot,)) #
+ # BOT_SCHEDULER.add_job(tenders_sched_ap, 'cron', hour='6', minute='0', args=(bot,)) #
+
+# scheduler.add_job(func, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2021-05-30')
+
+
+# Запуск бота и базовых функций
+async def main():
+ BOT_SCHEDULER.start() # Запуск Шедулера
+ dp = Dispatcher() # Образ Диспетчера
+ arSession = AsyncRequestSession() # Пул асинхронной сессии запросов
+ bot = Bot( # Образ Бота
+ token=BOT_TOKEN,
+ default=DefaultBotProperties(
+ parse_mode="HTML",
+ )
+ )
+
+ register_all_middlwares(dp) # Регистрация всех мидлварей
+ register_all_routers(dp) # Регистрация всех роутеров
+
+ try:
+ await set_commands(bot) # Установка команд
+ await startup_notify(bot) # Уведомления админам при запуске бота
+ await scheduler_start(bot) # Подключение шедулеров
+
+ bot_logger.warning("BOT WAS STARTED")
+ print(colorama.Fore.LIGHTYELLOW_EX + f"~~~~~ Bot was started - @{(await bot.get_me()).username} ~~~~~")
+ print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ ~~~~~")
+ print(colorama.Fore.RESET)
+
+ if len(get_admins()) == 0: print("***** ENTER ADMIN ID IN settings.ini *****")
+
+ await bot.delete_webhook() # Удаление вебхуков, если они имеются
+ await bot.get_updates(offset=-1) # Сброс пендинг апдейтов
+
+ await dp.start_polling(
+ bot,
+ arSession=arSession,
+ allowed_updates=dp.resolve_used_update_types(),
+ )
+ finally:
+ await arSession.close()
+ await bot.session.close()
+
+
+if __name__ == "__main__":
+ create_dbx()
+
+ try:
+ asyncio.run(main())
+ except (KeyboardInterrupt, SystemExit):
+ bot_logger.warning("Bot was stopped")
+ # finally:
+ # if sys.platform.startswith("win"):
+ # os.system("cls")
+ # else:
+ # os.system("clear")
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..d81f733
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,97 @@
+aiofiles==23.2.1
+aiogram==3.4.1
+aiohttp==3.9.3
+aiosignal==1.3.1
+annotated-types==0.6.0
+anyio==4.3.0
+appnope==0.1.4
+APScheduler==3.10.4
+asttokens==2.4.1
+async-timeout==4.0.3
+attrs==23.2.0
+beautifulsoup4==4.12.3
+bs4==0.0.2
+cachetools==5.3.3
+certifi==2024.2.2
+charset-normalizer==3.3.2
+click==8.1.7
+colorama==0.4.6
+colorlog==6.8.2
+comm==0.2.2
+contourpy==1.2.1
+cycler==0.12.1
+debugpy==1.8.1
+decorator==5.1.1
+et-xmlfile==1.1.0
+exceptiongroup==1.2.0
+executing==2.0.1
+fastapi==0.110.1
+fastjsonschema==2.19.1
+fonttools==4.51.0
+frozenlist==1.4.1
+h11==0.14.0
+idna==3.7
+ipykernel==6.29.4
+ipython==8.23.0
+jedi==0.19.1
+Jinja2==3.1.3
+jsonschema==4.21.1
+jsonschema-specifications==2023.12.1
+jupyter_client==8.6.1
+jupyter_core==5.7.2
+kiwisolver==1.4.5
+magic-filter==1.0.12
+MarkupSafe==2.1.5
+matplotlib==3.8.4
+matplotlib-inline==0.1.6
+multidict==6.0.5
+nbformat==5.10.4
+nest-asyncio==1.6.0
+numpy==1.26.4
+openpyxl==3.1.2
+packaging==24.0
+pandas==2.2.2
+parso==0.8.4
+pexpect==4.9.0
+pillow==10.3.0
+platformdirs==4.2.0
+plotly==5.21.0
+prompt-toolkit==3.0.43
+psutil==5.9.8
+ptyprocess==0.7.0
+pure-eval==0.2.2
+pybit==5.6.2
+pycryptodome==3.20.0
+pydantic
+pydantic_core
+Pygments==2.17.2
+pyparsing==3.1.2
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.1
+pytz==2024.1
+pyzmq==25.1.2
+rapidfuzz==3.9.0
+referencing==0.34.0
+requests==2.31.0
+rpds-py==0.18.0
+scipy==1.13.0
+six==1.16.0
+sniffio==1.3.1
+soupsieve==2.5
+stack-data==0.6.3
+starlette==0.37.2
+ta==0.11.0
+tenacity==8.2.3
+thefuzz==0.22.1
+tornado==6.4
+traitlets==5.14.2
+typing==3.7.4.3
+typing_extensions==4.11.0
+tzdata==2024.1
+tzlocal==5.2
+urllib3==2.2.1
+uvicorn==0.20.0
+wcwidth==0.2.13
+websocket-client==1.7.0
+websockets==12.0
+yarl==1.9.4
diff --git a/settings.ini b/settings.ini
new file mode 100644
index 0000000..984e050
--- /dev/null
+++ b/settings.ini
@@ -0,0 +1,3 @@
+[settings]
+bot_token=7096948690:AAGe_b8fOKsLSvjK-Yk703JhEN3ySaqucKo
+admin_id=340394898
diff --git a/tgbot/.DS_Store b/tgbot/.DS_Store
new file mode 100644
index 0000000..e01d069
Binary files /dev/null and b/tgbot/.DS_Store differ
diff --git a/tgbot/__init__.py b/tgbot/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/tgbot/__init__.py
@@ -0,0 +1 @@
+
diff --git a/tgbot/data/__init__.py b/tgbot/data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/data/config.py b/tgbot/data/config.py
new file mode 100644
index 0000000..206b3e1
--- /dev/null
+++ b/tgbot/data/config.py
@@ -0,0 +1,42 @@
+# - *- coding: utf- 8 - *-
+import configparser
+
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+
+# Токен бота
+BOT_TOKEN = configparser.ConfigParser()
+BOT_TOKEN.read("settings.ini")
+BOT_TOKEN = BOT_TOKEN['settings']['bot_token'].strip().replace(' ', '')
+BOT_TIMEZONE = "Asia/Krasnoyarsk" # Временная зона бота
+
+# Пути к файлам
+PATH_DATABASE = "tgbot/data/database.db" # Путь к БД
+PATH_LOGS = "tgbot/data/logs.log" # Путь к Логам
+PATH_EXCEL = "tgbot/data/articles_sheet.xlsx" # Путь к таблице с артиклами
+
+# Образы и конфиги
+BOT_SCHEDULER = AsyncIOScheduler(timezone=BOT_TIMEZONE) # Образ шедулера
+start_status = True # Оповещение админам о запуске бота (True или False)
+
+
+# Получение администраторов бота
+def get_admins() -> list[int]:
+ read_admins = configparser.ConfigParser()
+ read_admins.read('settings.ini')
+
+ admins = read_admins['settings']['admin_id'].strip().replace(" ", "")
+
+ if "," in admins:
+ admins = admins.split(",")
+ else:
+ if len(admins) >= 1:
+ admins = [admins]
+ else:
+ admins = []
+
+ while "" in admins: admins.remove("")
+ while " " in admins: admins.remove(" ")
+ while "," in admins: admins.remove(",")
+ while "\r" in admins: admins.remove("\r")
+
+ return list(map(int, admins))
diff --git a/tgbot/database/__init__.py b/tgbot/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/database/db_helper.py b/tgbot/database/db_helper.py
new file mode 100644
index 0000000..f3df718
--- /dev/null
+++ b/tgbot/database/db_helper.py
@@ -0,0 +1,124 @@
+# - *- coding: utf- 8 - *-
+import sqlite3
+
+from tgbot.data.config import PATH_DATABASE
+from tgbot.utils.const_functions import ded
+
+
+# Преобразование полученного списка в словарь
+def dict_factory(cursor, row) -> dict:
+ save_dict = {}
+
+ for idx, col in enumerate(cursor.description):
+ save_dict[col[0]] = row[idx]
+
+ return save_dict
+
+
+# Форматирование запроса без аргументов
+def update_format(sql, parameters: dict) -> tuple[str, list]:
+ values = ", ".join([
+ f"{item} = ?" for item in parameters
+ ])
+ sql += f" {values}"
+
+ return sql, list(parameters.values())
+
+
+# Форматирование запроса с аргументами
+def update_format_where(sql, parameters: dict) -> tuple[str, list]:
+ sql += " WHERE "
+
+ sql += " AND ".join([
+ f"{item} = ?" for item in parameters
+ ])
+
+ return sql, list(parameters.values())
+
+
+################################################################################
+# Создание всех таблиц для БД
+def create_dbx():
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+
+ ############################################################
+ # Создание таблицы с хранением - пользователей
+ if len(con.execute("PRAGMA table_info(storage_users)").fetchall()) == 8:
+ print("DB was found(1/4)")
+ else:
+ con.execute(
+ ded(f"""
+ CREATE TABLE storage_users(
+ increment INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER,
+ user_login TEXT,
+ user_name TEXT,
+ user_surname TEXT,
+ user_fullname TEXT,
+ notif TEXT,
+ user_unix INTEGER
+ )
+ """)
+ )
+ print("DB was not found(1/4) | Creating...")
+
+ # Создание таблицы с хранением - настроек
+ if len(con.execute("PRAGMA table_info(storage_settings)").fetchall()) == 2: #!!!!!
+ print("DB was found(2/4)")
+ else:
+ con.execute(
+ ded(f"""
+ CREATE TABLE storage_settings(
+ status_work TEXT,
+ status_sched_tenders TEXT
+ )
+ """)
+ )
+
+ con.execute(
+ ded(f"""
+ INSERT INTO storage_settings(
+ status_work,
+ status_sched_tenders
+ )
+ VALUES (?,?)
+ """),
+ [
+ 'True',
+ 'True'
+ ]
+ )
+ print("DB was not found(2/4) | Creating...")
+
+ # Создание таблицы с хранением - тендеров
+ if len(con.execute("PRAGMA table_info(storage_tenders)").fetchall()) == 5:
+ print("DB was found(3/4)")
+ else:
+ con.execute(
+ ded(f"""
+ CREATE TABLE storage_tenders(
+ tender_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ tender_name TEXT,
+ tender_link TEXT,
+ date_creat DATE,
+ date_until DATE
+ )
+ """)
+ )
+ print("DB was not found(3/4) | Creating...")
+
+ # Создание таблицы с хранением - товаров
+ if len(con.execute("PRAGMA table_info(storage_goods)").fetchall()) == 3:
+ print("DB was found(4/4)")
+ else:
+ con.execute(
+ ded(f"""
+ CREATE TABLE storage_goods(
+ good_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ good_name TEXT,
+ tender_id INTEGER REFERENCES storage_tenders(tender_id) ON UPDATE CASCADE
+ )
+ """)
+ )
+ print("DB was not found(4/4) | Creating...")
\ No newline at end of file
diff --git a/tgbot/database/db_settings.py b/tgbot/database/db_settings.py
new file mode 100644
index 0000000..d0fd407
--- /dev/null
+++ b/tgbot/database/db_settings.py
@@ -0,0 +1,37 @@
+# - *- coding: utf- 8 - *-
+import sqlite3
+
+from pydantic import BaseModel
+
+from tgbot.data.config import PATH_DATABASE
+from tgbot.database.db_helper import dict_factory, update_format
+
+
+# Модель таблицы
+class SettingsModel(BaseModel):
+ status_work: str
+ status_sched_tenders: str
+
+
+# Работа с настройками
+class Settingsx:
+ storage_name = "storage_settings"
+
+ # Получение записи
+ @staticmethod
+ def get() -> SettingsModel:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Settingsx.storage_name}"
+
+ return SettingsModel(**con.execute(sql).fetchone())
+
+ # Редактирование записи
+ @staticmethod
+ def update(**kwargs):
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"UPDATE {Settingsx.storage_name} SET"
+ sql, parameters = update_format(sql, kwargs)
+
+ con.execute(sql, parameters)
diff --git a/tgbot/database/db_tenders.py b/tgbot/database/db_tenders.py
new file mode 100644
index 0000000..c717359
--- /dev/null
+++ b/tgbot/database/db_tenders.py
@@ -0,0 +1,137 @@
+# - *- coding: utf- 8 - *-
+import sqlite3
+
+from pydantic import BaseModel
+
+from tgbot.data.config import PATH_DATABASE
+from tgbot.database.db_helper import dict_factory, update_format_where, update_format
+from tgbot.utils.const_functions import get_unix, ded
+
+
+# Модель таблицы
+class UserModel(BaseModel):
+ increment: int
+ user_id: int
+ user_login: str
+ user_name: str
+ user_surname: str
+ user_fullname: str
+ notif: str
+ user_unix: int
+
+
+# Работа с юзером
+class Userx:
+ storage_name = "storage_users"
+
+ # Добавление записи
+ @staticmethod
+ def add(
+ user_id: int,
+ user_login: str,
+ user_name: str,
+ user_surname: str,
+ user_fullname: str,
+ notif: str = "False",
+ ):
+ user_unix = get_unix()
+
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+
+ con.execute(
+ ded(f"""
+ INSERT INTO {Userx.storage_name} (
+ user_id,
+ user_login,
+ user_name,
+ user_surname,
+ user_fullname,
+ notif,
+ user_unix
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ """),
+ [
+ user_id,
+ user_login,
+ user_name,
+ user_surname,
+ user_fullname,
+ notif,
+ user_unix,
+ ],
+ )
+
+ # Получение записи
+ @staticmethod
+ def get(**kwargs) -> UserModel:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Userx.storage_name}"
+ sql, parameters = update_format_where(sql, kwargs)
+
+ response = con.execute(sql, parameters).fetchone()
+
+ if response is not None:
+ response = UserModel(**response)
+
+ return response
+
+ # Получение записей
+ @staticmethod
+ def gets(**kwargs) -> list[UserModel]:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Userx.storage_name}"
+ sql, parameters = update_format_where(sql, kwargs)
+
+ response = con.execute(sql, parameters).fetchall()
+
+ if len(response) >= 1:
+ response = [UserModel(**cache_object) for cache_object in response]
+
+ return response
+
+ # Получение всех записей
+ @staticmethod
+ def get_all() -> list[UserModel]:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Userx.storage_name}"
+
+ response = con.execute(sql).fetchall()
+
+ if len(response) >= 1:
+ response = [UserModel(**cache_object) for cache_object in response]
+
+ return response
+
+ # Редактирование записи
+ @staticmethod
+ def update(user_id, **kwargs):
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"UPDATE {Userx.storage_name} SET"
+ sql, parameters = update_format(sql, kwargs)
+ parameters.append(user_id)
+
+ con.execute(sql + "WHERE user_id = ?", parameters)
+
+ # Удаление записи
+ @staticmethod
+ def delete(**kwargs):
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"DELETE FROM {Userx.storage_name}"
+ sql, parameters = update_format_where(sql, kwargs)
+
+ con.execute(sql, parameters)
+
+ # Очистка всех записей
+ @staticmethod
+ def clear():
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"DELETE FROM {Userx.storage_name}"
+
+ con.execute(sql)
diff --git a/tgbot/database/db_users.py b/tgbot/database/db_users.py
new file mode 100644
index 0000000..c717359
--- /dev/null
+++ b/tgbot/database/db_users.py
@@ -0,0 +1,137 @@
+# - *- coding: utf- 8 - *-
+import sqlite3
+
+from pydantic import BaseModel
+
+from tgbot.data.config import PATH_DATABASE
+from tgbot.database.db_helper import dict_factory, update_format_where, update_format
+from tgbot.utils.const_functions import get_unix, ded
+
+
+# Модель таблицы
+class UserModel(BaseModel):
+ increment: int
+ user_id: int
+ user_login: str
+ user_name: str
+ user_surname: str
+ user_fullname: str
+ notif: str
+ user_unix: int
+
+
+# Работа с юзером
+class Userx:
+ storage_name = "storage_users"
+
+ # Добавление записи
+ @staticmethod
+ def add(
+ user_id: int,
+ user_login: str,
+ user_name: str,
+ user_surname: str,
+ user_fullname: str,
+ notif: str = "False",
+ ):
+ user_unix = get_unix()
+
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+
+ con.execute(
+ ded(f"""
+ INSERT INTO {Userx.storage_name} (
+ user_id,
+ user_login,
+ user_name,
+ user_surname,
+ user_fullname,
+ notif,
+ user_unix
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ """),
+ [
+ user_id,
+ user_login,
+ user_name,
+ user_surname,
+ user_fullname,
+ notif,
+ user_unix,
+ ],
+ )
+
+ # Получение записи
+ @staticmethod
+ def get(**kwargs) -> UserModel:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Userx.storage_name}"
+ sql, parameters = update_format_where(sql, kwargs)
+
+ response = con.execute(sql, parameters).fetchone()
+
+ if response is not None:
+ response = UserModel(**response)
+
+ return response
+
+ # Получение записей
+ @staticmethod
+ def gets(**kwargs) -> list[UserModel]:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Userx.storage_name}"
+ sql, parameters = update_format_where(sql, kwargs)
+
+ response = con.execute(sql, parameters).fetchall()
+
+ if len(response) >= 1:
+ response = [UserModel(**cache_object) for cache_object in response]
+
+ return response
+
+ # Получение всех записей
+ @staticmethod
+ def get_all() -> list[UserModel]:
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"SELECT * FROM {Userx.storage_name}"
+
+ response = con.execute(sql).fetchall()
+
+ if len(response) >= 1:
+ response = [UserModel(**cache_object) for cache_object in response]
+
+ return response
+
+ # Редактирование записи
+ @staticmethod
+ def update(user_id, **kwargs):
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"UPDATE {Userx.storage_name} SET"
+ sql, parameters = update_format(sql, kwargs)
+ parameters.append(user_id)
+
+ con.execute(sql + "WHERE user_id = ?", parameters)
+
+ # Удаление записи
+ @staticmethod
+ def delete(**kwargs):
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"DELETE FROM {Userx.storage_name}"
+ sql, parameters = update_format_where(sql, kwargs)
+
+ con.execute(sql, parameters)
+
+ # Очистка всех записей
+ @staticmethod
+ def clear():
+ with sqlite3.connect(PATH_DATABASE) as con:
+ con.row_factory = dict_factory
+ sql = f"DELETE FROM {Userx.storage_name}"
+
+ con.execute(sql)
diff --git a/tgbot/keyboards/__init__.py b/tgbot/keyboards/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/keyboards/inline_main.py b/tgbot/keyboards/inline_main.py
new file mode 100644
index 0000000..1cd47ea
--- /dev/null
+++ b/tgbot/keyboards/inline_main.py
@@ -0,0 +1,26 @@
+# - *- coding: utf- 8 - *-
+from aiogram.types import InlineKeyboardMarkup
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from tgbot.data.config import get_admins
+from tgbot.utils.const_functions import ikb
+
+
+# Кнопки инлайн меню
+def menu_finl(user_id: int) -> InlineKeyboardMarkup:
+ keyboard = InlineKeyboardBuilder()
+
+ # keyboard.row(
+ # ikb("User X", data="user_inline_x"),
+ # ikb("User 1", data="user_inline:user_btn"),
+ # ikb("User 2", data="..."),
+ # )
+
+ # if user_id in get_admins():
+ # keyboard.row(
+ # ikb("Admin X", data="admin_inline_x"),
+ # ikb("Admin 1", data="admin_inline:admin_btn"),
+ # ikb("Admin 2", data="unknown"),
+ # )
+
+ return keyboard.as_markup()
diff --git a/tgbot/keyboards/inline_misc.py b/tgbot/keyboards/inline_misc.py
new file mode 100644
index 0000000..ffb83dc
--- /dev/null
+++ b/tgbot/keyboards/inline_misc.py
@@ -0,0 +1,22 @@
+# - *- coding: utf- 8 - *-
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from tgbot.utils.const_functions import ikb
+
+# Тестовые админ инлайн кнопки
+admin_inl = InlineKeyboardBuilder(
+).row(
+ # ikb("Admin Inline 1", data="..."),
+ # ikb("Admin Inline 2", data="..."),
+).row(
+ # ikb("Admin Inline 3", data="..."),
+).as_markup()
+
+# Тестовые юзер инлайн кнопки
+user_inl = InlineKeyboardBuilder(
+).row(
+ # ikb("User Inline 1", data="..."),
+ # ikb("User Inline 2", data="..."),
+).row(
+ # ikb("User Inline 3", data="..."),
+).as_markup()
diff --git a/tgbot/keyboards/reply_main.py b/tgbot/keyboards/reply_main.py
new file mode 100644
index 0000000..7587159
--- /dev/null
+++ b/tgbot/keyboards/reply_main.py
@@ -0,0 +1,49 @@
+# - *- coding: utf- 8 - *-
+from aiogram.types import ReplyKeyboardMarkup
+from aiogram.utils.keyboard import ReplyKeyboardBuilder
+
+from tgbot.data.config import get_admins
+from tgbot.utils.const_functions import rkb
+
+
+# Кнопки главного меню
+def menu_frep(user_id: int) -> ReplyKeyboardMarkup:
+ keyboard = ReplyKeyboardBuilder()
+
+ # BotCommand(command="start", description="♻️ Restart bot"),
+ # BotCommand(command="parser", description="Запускает поиск тендоров"),
+ # BotCommand(command="status", description="Статус бота"),
+ # BotCommand(command="get_notif", description="Получать уведомления"),
+ # BotCommand(command="stop_get", description="Остановить получение уведомлений"),
+ # BotCommand(command="start_shed", description="Запуск работы по расписанию"),
+ # BotCommand(command="stop_shed", description="Остановка работы по расписанию"),
+ # # BotCommand(command="inline", description="🌀 Get Inline keyboard"),
+ # BotCommand(command="log", description="🖨 Get Logs"),
+ # BotCommand(command="db", description="📦 Get Database"),
+
+ keyboard.row(
+ rkb("Поиск в tenderpro"), rkb("Tendrepro поиск за все время "),
+ )
+
+ keyboard.row(
+ rkb("Поиск в tenderplan"), rkb("Показать tenderplan"),
+ )
+
+ # keyboard.row(
+ # rkb("Поиск в tenderplan"),
+ # )
+
+ keyboard.row(
+ rkb("Получать уведомления"), rkb("Не получать уведомления"), rkb("Показать таблицу"), rkb("Статус бота"), rkb("Показать автопитер"),
+ )
+
+ # keyboard.row(
+ # rkb("Показать таблицу"), rkb("Статус бота"),
+ # )
+
+ # if user_id in get_admins():
+ # keyboard.row(
+ # rkb("Admin Inline"), rkb("Admin Reply"),
+ # )
+
+ return keyboard.as_markup(resize_keyboard=True)
diff --git a/tgbot/keyboards/reply_misc.py b/tgbot/keyboards/reply_misc.py
new file mode 100644
index 0000000..c14c89f
--- /dev/null
+++ b/tgbot/keyboards/reply_misc.py
@@ -0,0 +1,22 @@
+# - *- coding: utf- 8 - *-
+from aiogram.utils.keyboard import ReplyKeyboardBuilder
+
+from tgbot.utils.const_functions import rkb
+
+# Тестовые админ реплай кнопки
+admin_rep = ReplyKeyboardBuilder(
+).row(
+ # rkb("Admin Reply 1"),
+ # rkb("Admin Reply 2"),
+).row(
+ # rkb("🔙 Main menu"),
+).as_markup(resize_keyboard=True)
+
+# Тестовые юзер реплай кнопки
+user_rep = ReplyKeyboardBuilder(
+).row(
+ # rkb("User Reply 1"),
+ # rkb("User Reply 2"),
+).row(
+ # rkb("🔙 Main menu"),
+).as_markup(resize_keyboard=True)
diff --git a/tgbot/middlewares/__init__.py b/tgbot/middlewares/__init__.py
new file mode 100644
index 0000000..7602a1d
--- /dev/null
+++ b/tgbot/middlewares/__init__.py
@@ -0,0 +1,13 @@
+# - *- coding: utf- 8 - *-
+from aiogram import Dispatcher
+
+from tgbot.middlewares.middleware_user import ExistsUserMiddleware
+from tgbot.middlewares.middleware_throttling import ThrottlingMiddleware
+
+
+# Регистрация всех миддлварей
+def register_all_middlwares(dp: Dispatcher):
+ dp.callback_query.outer_middleware(ExistsUserMiddleware())
+ dp.message.outer_middleware(ExistsUserMiddleware())
+
+ dp.message.middleware(ThrottlingMiddleware())
diff --git a/tgbot/middlewares/middleware_throttling.py b/tgbot/middlewares/middleware_throttling.py
new file mode 100644
index 0000000..4aa814a
--- /dev/null
+++ b/tgbot/middlewares/middleware_throttling.py
@@ -0,0 +1,63 @@
+# - *- coding: utf- 8 - *-
+import time
+from typing import Any, Awaitable, Callable, Dict, Union
+
+from aiogram import BaseMiddleware
+from aiogram.dispatcher.flags import get_flag
+from aiogram.types import Message, User
+from cachetools import TTLCache
+
+
+# Антиспам
+class ThrottlingMiddleware(BaseMiddleware):
+ def __init__(self, default_rate: Union[int, float] = 1) -> None:
+ self.default_rate = default_rate
+
+ self.users = TTLCache(maxsize=10_000, ttl=600)
+
+ async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data):
+ this_user: User = data.get("event_from_user")
+
+ if get_flag(data, "rate") is not None:
+ self.default_rate = get_flag(data, "rate")
+
+ if self.default_rate == 0:
+ return await handler(event, data)
+
+ if this_user.id not in self.users:
+ self.users[this_user.id] = {
+ 'last_throttled': int(time.time()),
+ 'count_throttled': 0,
+ 'now_rate': self.default_rate,
+ }
+
+ return await handler(event, data)
+ else:
+ if int(time.time()) - self.users[this_user.id]['last_throttled'] >= self.users[this_user.id]['now_rate']:
+ self.users.pop(this_user.id)
+
+ return await handler(event, data)
+ else:
+ self.users[this_user.id]['last_throttled'] = int(time.time())
+
+ if self.users[this_user.id]['count_throttled'] == 0:
+ self.users[this_user.id]['count_throttled'] = 1
+ self.users[this_user.id]['now_rate'] = self.default_rate + 2
+
+ return await handler(event, data)
+ elif self.users[this_user.id]['count_throttled'] == 1:
+ self.users[this_user.id]['count_throttled'] = 2
+ self.users[this_user.id]['now_rate'] = self.default_rate + 3
+
+ await event.reply(
+ "❗ Пожалуйста, не спамьте.\n"
+ "❗ Please, do not spam.",
+ )
+ elif self.users[this_user.id]['count_throttled'] == 2:
+ self.users[this_user.id]['count_throttled'] = 3
+ self.users[this_user.id]['now_rate'] = self.default_rate + 5
+
+ await event.reply(
+ "❗ Бот не будет отвечать до прекращения спама.\n"
+ "❗ The bot will not respond until the spam stops.",
+ )
diff --git a/tgbot/middlewares/middleware_user.py b/tgbot/middlewares/middleware_user.py
new file mode 100644
index 0000000..ad10d25
--- /dev/null
+++ b/tgbot/middlewares/middleware_user.py
@@ -0,0 +1,48 @@
+# - *- coding: utf- 8 - *-
+from aiogram import BaseMiddleware
+
+from tgbot.database.db_users import Userx
+from tgbot.utils.const_functions import clear_html
+
+
+# Проверка юзера в БД и его добавление
+class ExistsUserMiddleware(BaseMiddleware):
+ async def __call__(self, handler, event, data):
+ this_user = data.get("event_from_user")
+
+ if not this_user.is_bot:
+ get_user = Userx.get(user_id=this_user.id)
+
+ user_id = this_user.id
+ user_login = this_user.username
+ user_name = clear_html(this_user.first_name)
+ user_surname = clear_html(this_user.last_name)
+ user_fullname = clear_html(this_user.first_name)
+ user_language = this_user.language_code
+
+ if user_login is None: user_login = ""
+ if user_name is None: user_name = ""
+ if user_surname is None: user_surname = ""
+ if user_fullname is None: user_fullname = ""
+ if user_language != "ru": user_language = "en"
+
+ if len(user_surname) >= 1: user_fullname += f" {user_surname}"
+
+ if get_user is None:
+ Userx.add(user_id, user_login.lower(), user_name, user_surname, user_fullname)
+ else:
+ if user_name != get_user.user_name:
+ Userx.update(get_user.user_id, user_name=user_name)
+
+ if user_surname != get_user.user_surname:
+ Userx.update(get_user.user_id, user_surname=user_surname)
+
+ if user_fullname != get_user.user_fullname:
+ Userx.update(get_user.user_id, user_fullname=user_fullname)
+
+ if user_login.lower() != get_user.user_login:
+ Userx.update(get_user.user_id, user_login=user_login.lower())
+
+ data['User'] = Userx.get(user_id=user_id)
+
+ return await handler(event, data)
diff --git a/tgbot/routers/.DS_Store b/tgbot/routers/.DS_Store
new file mode 100644
index 0000000..81a3437
Binary files /dev/null and b/tgbot/routers/.DS_Store differ
diff --git a/tgbot/routers/__init__.py b/tgbot/routers/__init__.py
new file mode 100644
index 0000000..bb7d82c
--- /dev/null
+++ b/tgbot/routers/__init__.py
@@ -0,0 +1,30 @@
+# - *- coding: utf- 8 - *-
+from aiogram import Dispatcher, F
+
+from tgbot.routers import main_errors, main_missed, main_start
+from tgbot.routers.admin import admin_menu
+from tgbot.routers.user import user_menu
+from tgbot.utils.misc.bot_filters import IsAdmin
+
+
+# Регистрация всех роутеров
+def register_all_routers(dp: Dispatcher):
+ # Подключение фильтров
+ main_errors.router.message.filter(F.chat.type == "private")
+ main_start.router.message.filter(F.chat.type == "private")
+
+ user_menu.router.message.filter(F.chat.type == "private")
+ admin_menu.router.message.filter(F.chat.type == "private", IsAdmin())
+
+ main_missed.router.message.filter(F.chat.type == "private")
+
+ # Подключение обязательных роутеров
+ dp.include_router(main_errors.router) # Роутер ошибки
+ dp.include_router(main_start.router) # Роутер основных команд
+
+ # Подключение пользовательских роутеров (юзеров и админов)
+ dp.include_router(user_menu.router) # Юзер роутер
+ dp.include_router(admin_menu.router) # Админ роутер
+
+ # Подключение обязательных роутеров
+ dp.include_router(main_missed.router) # Роутер пропущенных апдейтов
diff --git a/tgbot/routers/admin/__init__.py b/tgbot/routers/admin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/routers/admin/admin_menu.py b/tgbot/routers/admin/admin_menu.py
new file mode 100644
index 0000000..cd0352f
--- /dev/null
+++ b/tgbot/routers/admin/admin_menu.py
@@ -0,0 +1,121 @@
+# - *- coding: utf- 8 - *-
+import os
+
+import aiofiles
+from aiogram import Router, Bot, F
+from aiogram.filters import Command
+from aiogram.types import FSInputFile, Message, CallbackQuery
+from aiogram.utils.media_group import MediaGroupBuilder
+
+from tgbot.data.config import PATH_DATABASE, PATH_LOGS
+from tgbot.database.db_users import UserModel
+from tgbot.keyboards.inline_misc import admin_inl
+from tgbot.keyboards.reply_misc import admin_rep
+from tgbot.utils.const_functions import get_date
+from tgbot.utils.misc_functions import send_employees
+from tgbot.utils.misc.bot_models import FSM, ARS
+from tgbot.utils.misc.bot_logging import bot_logger
+from tgbot.services.parser_tendors import get_tenders_from_url
+
+
+router = Router(name=__name__)
+
+
+# # start shed
+# @router.message(F.text.in_(('start_shed')))
+# @router.message(Command(commands=['start_shed']))
+# async def parser(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# bot_logger.warning(f"command start_shed from {User.user_name}")
+
+# await send_employees(bot,"ss")
+
+# tenders_id = get_tenders_from_url()
+# bot_logger.warning(f"tenders_id: {tenders_id}")
+# answ = ""
+# for num, tend in enumerate(tenders_id):
+# answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, url: {tend['url_tender']} \n \n"
+
+# await message.answer(answ)
+
+
+
+# # Кнопка - Admin Inline
+# @router.message(F.text == 'Admin Inline')
+# async def admin_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await state.clear()
+
+# await message.answer("Click Button - Admin Inline", reply_markup=admin_inl)
+
+
+# # Кнопка - Admin Reply
+# @router.message(F.text == 'Admin Reply')
+# async def admin_button_reply(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await state.clear()
+
+# await message.answer("Click Button - Admin Reply", reply_markup=admin_rep)
+
+
+# # Колбэк - Admin X
+# @router.callback_query(F.data == 'admin_inline_x')
+# async def admin_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await call.answer(f"Click Admin X")
+
+
+# # Колбэк - Admin
+# @router.callback_query(F.data.startswith('admin_inline:'))
+# async def admin_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# get_data = call.data.split(":")[1]
+
+# await call.answer(f"Click Admin - {get_data}", True)
+
+
+# Получение Базы Данных
+@router.message(Command(commands=['db', 'database']))
+async def admin_database(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+
+ await message.answer_document(
+ FSInputFile(PATH_DATABASE),
+ caption=f"📦 #BACKUP | {get_date()}",
+ )
+
+
+# # Получение логов
+# @router.message(Command(commands=['log', 'logs']))
+# async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await state.clear()
+
+# media_group = MediaGroupBuilder(
+# caption=f"🖨 #LOGS | {get_date(full=False)}",
+# )
+
+# if os.path.isfile(PATH_LOGS):
+# media_group.add_document(media=FSInputFile(PATH_LOGS))
+
+# if os.path.isfile("tgbot/data/sv_log_err.log"):
+# media_group.add_document(media=FSInputFile("tgbot/data/sv_log_err.log"))
+
+# if os.path.isfile("tgbot/data/sv_log_out.log"):
+# media_group.add_document(media=FSInputFile("tgbot/data/sv_log_out.log"))
+
+# await message.answer_media_group(media=media_group.build())
+
+
+# Очистить логи
+@router.message(Command(commands=['clear_log', 'clear_logs', 'log_clear', 'logs_clear']))
+async def admin_logs_clear(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+
+ if os.path.isfile(PATH_LOGS):
+ async with aiofiles.open(PATH_LOGS, "w") as file:
+ await file.write(f"{get_date()} | LOGS WAS CLEAR")
+
+ if os.path.isfile("tgbot/data/sv_log_err.log"):
+ async with aiofiles.open("tgbot/data/sv_log_err.log", "w") as file:
+ await file.write(f"{get_date()} | LOGS WAS CLEAR")
+
+ if os.path.isfile("tgbot/data/sv_log_out.log"):
+ async with aiofiles.open("tgbot/data/sv_log_out.log", "w") as file:
+ await file.write(f"{get_date()} | LOGS WAS CLEAR")
+
+ await message.answer("🖨 The logs have been cleared")
diff --git a/tgbot/routers/main_errors.py b/tgbot/routers/main_errors.py
new file mode 100644
index 0000000..556365a
--- /dev/null
+++ b/tgbot/routers/main_errors.py
@@ -0,0 +1,29 @@
+# - *- coding: utf- 8 - *-
+from aiogram import Router
+from aiogram.filters import ExceptionMessageFilter
+from aiogram.handlers import ErrorHandler
+
+from tgbot.utils.misc.bot_logging import bot_logger
+
+router = Router(name=__name__)
+
+
+# Ошибка с блокировкой бота пользователем
+# @router.errors(ExceptionTypeFilter(TelegramForbiddenError))
+# class MyHandler(ErrorHandler):
+# async def handle(self):
+# pass
+
+
+# Ошибка с редактированием одинакового сообщения
+@router.errors(ExceptionMessageFilter(
+ "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")
+)
+class MyHandler(ErrorHandler):
+ async def handle(self):
+ bot_logger.exception(
+ f"====================\n"
+ f"Exception name: {self.exception_name}\n"
+ f"Exception message: {self.exception_message}\n"
+ f"===================="
+ )
diff --git a/tgbot/routers/main_missed.py b/tgbot/routers/main_missed.py
new file mode 100644
index 0000000..f7b312b
--- /dev/null
+++ b/tgbot/routers/main_missed.py
@@ -0,0 +1,61 @@
+# - *- coding: utf- 8 - *-
+import io
+from aiogram import Router, Bot, F
+from aiogram.types import CallbackQuery, Message, BufferedInputFile
+import aiogram
+import pandas as pd
+from tgbot.database.db_users import UserModel
+from tgbot.utils.const_functions import del_message
+from tgbot.utils.misc.bot_models import FSM, ARS
+
+from tgbot.services.parser_tendors import get_tenders_from_article, get_excel_from_tenders
+
+router = Router(name=__name__)
+
+
+# Колбэк с удалением сообщения
+@router.callback_query(F.data == 'close_this')
+async def main_callback_close(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await del_message(call.message)
+
+
+# Колбэк с обработкой кнопки
+@router.callback_query(F.data == '...')
+async def main_callback_answer(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await call.answer(cache_time=30)
+
+
+# Колбэк с обработкой удаления сообщений потерявших стейт
+@router.callback_query()
+async def main_callback_missed(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await call.answer(f"❗️ Miss callback: {call.data}", True)
+
+
+# Обработка всех неизвестных команд
+@router.message()
+async def main_message_missed(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ try:
+ tenders = await get_tenders_from_article(message.text)
+ if (len(str(tenders))>2000):
+ tenders = pd.DataFrame(tenders)
+ get_excel_from_tenders(tenders)
+ with io.BytesIO() as output:
+ tenders.to_excel(output)
+ excel_data = output.getvalue()
+ file_excel = io.BytesIO(excel_data)
+ await message.answer_document(BufferedInputFile(file_excel.getvalue(), f"{message.text}.xlsx"), caption = f"Нашлось по запросу '{message.text}'")
+ else:
+ answ = ""
+ for num, tend in enumerate(tenders):
+ answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, прием до: {tend['date_until']}, url: {tend['url_tender']} \n"
+ mes = f"Нашлось по запросу {message.text}: \n"
+ if answ == "":
+ mes += "Ничего не найдено"
+ else:
+ mes += answ
+ await message.answer(f"{mes}")
+ # except aiogram.exceptions.TelegramBadRequest:
+ # await message.answer(f"Ошибка: wg mes too long, {len(str(tenders))}")
+ except Exception as e:
+ await message.answer(f"Ошибка: {e}")
+
diff --git a/tgbot/routers/main_start.py b/tgbot/routers/main_start.py
new file mode 100644
index 0000000..64cf3c6
--- /dev/null
+++ b/tgbot/routers/main_start.py
@@ -0,0 +1,235 @@
+# - *- coding: utf- 8 - *-
+import os
+import io
+import pandas as pd
+
+from aiogram import Router, Bot, F
+from aiogram.filters import Command
+from aiogram.types import FSInputFile, Message, CallbackQuery, BufferedInputFile
+
+from aiogram.utils.media_group import MediaGroupBuilder
+from tgbot.database.db_users import UserModel, Userx
+from tgbot.keyboards.reply_main import menu_frep
+from tgbot.utils.const_functions import ded
+from tgbot.utils.misc.bot_models import FSM, ARS
+from tgbot.utils.misc.bot_logging import bot_logger
+from tgbot.services.parser_tendors import get_tenders_from_url, get_excel_from_tenders, get_articles
+from tgbot.services.tender_plan import tenders_with_goods, search_in_tenderplan
+from tgbot.data.config import BOT_SCHEDULER, PATH_EXCEL, PATH_LOGS
+from tgbot.utils.const_functions import get_date
+
+router = Router(name=__name__)
+
+
+# Открытие главного меню
+@router.message(F.text.in_(()))
+@router.message(Command(commands=['start']))
+async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+
+ bot_logger.warning(f"command start from {User.user_name}")
+ await message.answer(
+ ded(f"""
+ Привет, {User.user_name}
+ Это бот для поиска тендеров на сайте Tender.pro
+ Введите /parser для поиска прямо сейчас (ищет долго, минут 10)
+ Или /get_notif для получения уведомления в 8 и 18 часов
+ Команда /status показывает статус бота
+ Поиск за все время выведет таблицу в которой будут тендеры за все время, не важно какой статус
+ Чтобы обновить таблицу по которой искать просто пришлите ее мне
+ """),
+ reply_markup=menu_frep(message.from_user.id),
+ )
+
+
+# parser
+@router.message(F.text.in_(('parser', 'Начать поиск сейчас', 'Поиск в tenderpro')))
+@router.message(Command(commands=['parser']))
+async def parser(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ try:
+ bot_logger.warning(f"command parser from {User.user_name}")
+ await message.answer("Идет поиск тендеров")
+ tenders_id = await get_tenders_from_url()
+ bot_logger.warning(f"tenders_id: {tenders_id}")
+ if (len(str(tenders_id))>4000):
+ tenders_id = pd.DataFrame(tenders_id)
+ get_excel_from_tenders(tenders_id)
+ with io.BytesIO() as output:
+ tenders_id.to_excel(output)
+ excel_data = output.getvalue()
+ file_excel = io.BytesIO(excel_data)
+ await message.answer_document(BufferedInputFile(file_excel.getvalue(), f"{message.text}.xlsx"), caption = f"Нашлось по запросу '{message.text}'")
+ else:
+ answ = ""
+ for num, tend in enumerate(tenders_id):
+ answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, прием до: {tend['date_until']}, url: {tend['url_tender']} \n \n"
+ mes = f"Ответ на запрос поиска тендеров: \n \n"
+ if answ == "":
+ mes += "Ничего не найдено"
+ else:
+ mes += answ
+ await message.answer(f"{mes}")
+ except Exception as e:
+ await message.answer(f"Ошибка: {e}")
+
+
+# status
+@router.message(F.text.in_(('status', 'Статус бота')))
+@router.message(Command(commands=['status']))
+async def status(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ bot_logger.warning(f"command status from {User.user_name}")
+ jobs = BOT_SCHEDULER.get_jobs()
+ bot_logger.warning(f"jobs: {jobs}")
+ await message.answer(f"jobs: \n{[str(j) for j in jobs]}")
+
+# start notification
+@router.message(F.text.in_(('Получать уведомления', 'get_notif')))
+@router.message(Command(commands=['get_notif']))
+async def get_notif(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ bot_logger.warning(f"command get_notif from {User.user_name}")
+
+ if User.notif == "False":
+ Userx.update(User.user_id, notif = "True")
+ await message.answer("Теперь вы будете получать уведомления")
+
+ else:
+ await message.answer("Вы уже получаете уведомления")
+
+# stop notification
+@router.message(F.text.in_(('Не получать уведомления', 'stop_get_notif')))
+@router.message(Command(commands=['stop_get_notif']))
+async def stop_get_notif(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ bot_logger.warning(f"command stop_get_notif from {User.user_name}")
+
+ if User.notif == "True":
+ Userx.update(User.user_id, notif = "False")
+ await message.answer("Теперь вы не будете получать уведомления")
+
+ else:
+ await message.answer("Вы и так не получаете уведомления")
+
+
+# Загрузка таблицы в бот
+@router.message(F.content_type.in_({'document', 'file'}))
+async def upload_excel(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ if (message.document.file_name.find('.xls') != -1):
+ file_id = message.document.file_id
+ file = await bot.get_file(file_id)
+ file_path = file.file_path
+ if message.document.file_name.find('articles_sheet') != -1:
+ link_temp = "tgbot/data/articles_sheet_temp.xlsx"
+ await bot.download_file(file_path, link_temp)
+ try:
+ arts = get_articles(link=link_temp)
+ bot_logger.warning(f"command upload_excel from {User.user_name}. файл: {message.document.file_name}, загружен")
+ await bot.download_file(file_path, "tgbot/data/articles_sheet.xlsx")
+ await message.answer("Таблица загружена")
+ except Exception as e:
+ bot_logger.warning(f"command upload_excel from {User.user_name}. файл: {message.document.file_name}, не загружен. Ошибка {e}")
+ await message.answer(f"Ошибка {e} \nФайл не был загружен")
+ else:
+ await bot.download_file(file_path, f"tgbot/data/price_{message.document.file_name}")
+ await message.answer(f"Таблица {message.document.file_name} загружена")
+
+ else:
+ bot_logger.warning(f"command upload_excel from {User.user_name}. файл: {message.document.file_name}, не загружен")
+ await message.answer("Файл должен быть с расширением .xls или .xlsx")
+
+
+# Выгрузка таблицы
+@router.message(F.text.in_(('Показать таблицу', 'get_articles')))
+@router.message(Command(commands=['get_sheet', 'article']))
+async def get_sheet(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+ await message.answer_document(
+ FSInputFile(PATH_EXCEL),
+ # caption=f"📦 #BACKUP | {get_date()}",
+ caption=f"Таблица, которую вы загрузили и по которой выполняется поиск",
+ )
+
+# Поиск тендеров за все время
+@router.message(F.text.in_(('Поиск за все время', 'excel_from_tenders')))
+@router.message(Command(commands=['excel_from_tenders', 'tenders']))
+async def excel_from_tenders(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+ tenders_id = await get_tenders_from_url(tender_state=100)
+ get_excel_from_tenders(tenders_id=tenders_id)
+
+ await message.answer_document(
+ FSInputFile('tgbot/data/tenders_id_all.xlsx'),
+ # caption=f"📦 #BACKUP | {get_date()}",
+ caption=f"Тендеры за все время.",
+ )
+
+
+# Получение логов
+@router.message(Command(commands=['log', 'logs']))
+async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+
+ media_group = MediaGroupBuilder(
+ caption=f"🖨 #LOGS | {get_date(full=False)}",
+ )
+
+ if os.path.isfile(PATH_LOGS):
+ media_group.add_document(media=FSInputFile(PATH_LOGS))
+
+ if os.path.isfile("tgbot/data/sv_log_err.log"):
+ media_group.add_document(media=FSInputFile("tgbot/data/sv_log_err.log"))
+
+ if os.path.isfile("tgbot/data/sv_log_out.log"):
+ media_group.add_document(media=FSInputFile("tgbot/data/sv_log_out.log"))
+
+ await message.answer_media_group(media=media_group.build())
+
+
+# Поиск тендеров в автопитере
+@router.message(F.text.in_(('Поиск в автопитере', 'tenders_with_goods')))
+@router.message(Command(commands=['tenders_with_goods', 'search_ap']))
+async def search_in_ap(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+ bot_logger.warning(f"command search_in_ap from {User.user_name}")
+ await message.answer("Идет поиск тендеров")
+ tenders_with_goods(1)
+ await message.answer_document(
+ FSInputFile('tgbot/data/tenders_with_goods.xlsx'),
+ caption=f"Тендеры в автопитере.",
+ )
+
+
+# таблица тендеров в автопитере
+@router.message(F.text.in_(('Показать автопитер', 'excel_ap')))
+@router.message(Command(commands=['excel_from_ap', 'show_ap']))
+async def excel_from_ap(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+ bot_logger.warning(f"command excel_from_ap from {User.user_name}")
+ await message.answer_document(
+ FSInputFile('tgbot/data/tenders_with_goods.xlsx'),
+ caption=f"Тендеры в автопитере.",
+ )
+
+
+# Поиск тендеров в tenderplan
+@router.message(F.text.in_(('Поиск в tenderplan', 'tenders_in_tenderplan')))
+@router.message(Command(commands=['tenders_in_tenderplan', 'search_in_tenderplan', 'tenderplan']))
+async def search_in_tenderplan1(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+ bot_logger.warning(f"command search_in_tenderplan from {User.user_name}")
+ await message.answer("Идет поиск тендеров")
+ await search_in_tenderplan()
+ await message.answer_document(
+ FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'),
+ caption=f"Тендеры в tenderplan.",
+ )
+
+
+# таблица тендеров в tenderplan
+@router.message(F.text.in_(('Показать tenderplan', 'excel_tenderplan')))
+@router.message(Command(commands=['excel_from_tenderplan', 'show_tenderplan']))
+async def excel_from_tenderplan(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+ await state.clear()
+ bot_logger.warning(f"command excel_from_tenderplan from {User.user_name}")
+ await message.answer_document(
+ FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'),
+ caption=f"Тендеры в tenderplan.",
+ )
\ No newline at end of file
diff --git a/tgbot/routers/user/__init__.py b/tgbot/routers/user/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/routers/user/user_menu.py b/tgbot/routers/user/user_menu.py
new file mode 100644
index 0000000..a38ac6f
--- /dev/null
+++ b/tgbot/routers/user/user_menu.py
@@ -0,0 +1,59 @@
+# - *- coding: utf- 8 - *-
+from aiogram import Router, Bot, F
+from aiogram.filters import Command
+from aiogram.types import Message, CallbackQuery
+
+from tgbot.database.db_users import UserModel
+from tgbot.keyboards.inline_main import menu_finl
+from tgbot.keyboards.inline_misc import user_inl
+from tgbot.keyboards.reply_misc import user_rep
+from tgbot.utils.misc.bot_models import FSM, ARS
+
+router = Router(name=__name__)
+
+
+# # Кнопка - User Inline
+# @router.message(F.text == 'User Inline')
+# async def user_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await state.clear()
+
+# await message.answer(
+# "Click Button - User Inline",
+# reply_markup=user_inl,
+# )
+
+
+# # Кнопка - User Reply
+# @router.message(F.text == 'User Reply')
+# async def user_button_reply(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await state.clear()
+
+# await message.answer(
+# "Click Button - User Reply",
+# reply_markup=user_rep,
+# )
+
+
+# # Команда - /inline
+# @router.message(Command(commands="inline"))
+# async def user_command_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await state.clear()
+
+# await message.answer(
+# "Click command - /inline",
+# reply_markup=menu_finl(message.from_user.id),
+# )
+
+
+# # Колбэк - User X
+# @router.callback_query(F.data == 'user_inline_x')
+# async def user_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# await call.answer(f"Click User X")
+
+
+# # Колбэк - User
+# @router.callback_query(F.data.startswith('user_inline:'))
+# async def user_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
+# get_data = call.data.split(":")[1]
+
+# await call.answer(f"Click User - {get_data}", True)
diff --git a/tgbot/services/__init__.py b/tgbot/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/services/api_session.py b/tgbot/services/api_session.py
new file mode 100644
index 0000000..738917b
--- /dev/null
+++ b/tgbot/services/api_session.py
@@ -0,0 +1,30 @@
+# - *- coding: utf- 8 - *-
+from typing import Optional
+
+import aiohttp
+
+
+# In handler
+# session = await arSession.get_session()
+# response = await session.get(...)
+# response = await session.post(...)
+
+# Асинхронная сессия для запросов
+class AsyncRequestSession:
+ def __init__(self) -> None:
+ self._session: Optional[aiohttp.ClientSession] = None
+
+ # Получение сессии
+ async def get_session(self) -> aiohttp.ClientSession:
+ if self._session is None:
+ new_session = aiohttp.ClientSession()
+ self._session = new_session
+
+ return self._session
+
+ # Закрытие сессии
+ async def close(self) -> None:
+ if self._session is None:
+ return None
+
+ await self._session.close()
diff --git a/tgbot/services/parser_tendors.py b/tgbot/services/parser_tendors.py
new file mode 100644
index 0000000..3819c87
--- /dev/null
+++ b/tgbot/services/parser_tendors.py
@@ -0,0 +1,242 @@
+import requests
+from bs4 import BeautifulSoup
+import asyncio
+from aiohttp import ClientSession
+import pandas as pd
+from time import time
+import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
+from tgbot.data.config import PATH_EXCEL
+from tgbot.utils.misc.bot_logging import bot_logger
+
+
+
+# arrt = [
+# {'Name': 'О-514002', 'articles': ['О-514002', 'JX0818', '61000070005', '3831236', 'W 962/6', 'OP592', 'C-6204']},
+# {'Name': 'F-714117', 'articles': ['F-714117', '612630080087', 'FC-71090', 'R010018']},
+# {'Name': 'F-742003', 'articles': ['F-742003', '612630080088', 'PL420', 'VG1540080032', 'A 960 477 00 03', 'PL 420/7X', 'SFC-7939-30B']},
+# {'Name': 'F-742003X', 'articles': ['F-742003X', '612630080088', 'PL420 с подогревом', 'VG1540080032', 'A960 477 00 03','PL 420/7X', 'SFC-7939-30B']},
+# ]
+
+# article = {
+# 'О-514002': ['О-514002', 'JX0818', '61000070005', '3831236', 'W 962/6', 'OP592', 'C-6204'],
+# 'F-714117': ['F-714117', '612630080087', 'FC-71090', 'R010018'],
+# 'F-742003': ['F-742003', '612630080088', 'PL420', 'VG1540080032', 'A 960 477 00 03', 'PL 420/7X', 'SFC-7939-30B'],
+# 'F-742003X': ['F-742003X', '612630080088', 'PL420 с подогревом', 'VG1540080032', 'A960 477 00 03','PL 420/7X', 'SFC-7939-30B'],
+# }
+
+# art1 = {
+# 'F-714117': ['612630080087', 'FC-71090', 'R010018'],
+# 'F-742003': ['612630080088', 'PL420', 'VG1540080032'],
+# }
+
+
+# def get_articles():
+# link = PATH_EXCEL
+# art = pd.read_excel(link, skiprows=1)
+# art = art.loc[:,["Наименование Wanlanda","Наименование аналога"]]
+# art["Наименование аналога"] = art["Наименование аналога"].str.split(", |,")
+# for a in art.iloc:
+# a["Наименование аналога"].append(a["Наименование Wanlanda"])
+# return art
+
+def get_articles(link = PATH_EXCEL):
+ # link = PATH_EXCEL
+ all_article = pd.DataFrame(index=[], columns=['Наименование', 'Артикул'])
+ excel_reader = pd.ExcelFile(link)
+ for sheet_name in excel_reader.sheet_names:
+ exc = excel_reader.parse(sheet_name, usecols=['Наименование', 'Артикул'])
+ # exc['Наименование'] = exc['Наименование'].str
+ exc['Наименование'] = sheet_name + " / " + exc['Наименование'].astype(str) # добавление названия листа к наименованию позиции
+ all_article = pd.concat([all_article,exc], ignore_index=True)
+ all_article = all_article.dropna(inplace=False)
+ all_article["Артикул"] = all_article["Артикул"].astype(str)
+ all_article["Артикул"] = all_article["Артикул"].str.split(", | ,")
+ # for a in art.iloc:
+ # a["Артикул"].append(a["Наименование"])
+ # print(f"колич артик:{len(all_article)}")
+ bot_logger.warning(f"колич артик:{len(all_article)}")
+ print(f"all_article: {all_article[:2]}")
+ return all_article[:]
+
+# для pd
+def get_urls(tender_state = 1, article = 0):
+ # tender_state = 1:открытые 100:все
+ urls = []
+ if (article == 0):
+ article = get_articles()
+ for val in article.iloc:
+ # print(val["Артикул"])
+ for art in val["Артикул"]:
+ urls.append({"article": f"{val['Наименование']}/{art}", "url": f"http://www.tender.pro/api/tenders/list?&good_name={art}&tender_state={tender_state}&by=1000"})
+ else:
+ articles = article.split(", |,")
+ for art in articles:
+ urls.append({"article": f"{art}", "url": f"http://www.tender.pro/api/tenders/list?&good_name={art}&tender_state={tender_state}&by=1000"})
+
+ return urls
+
+
+async def fetch(url, session):
+ async with session.get(url['url']) as response:
+ status = response.status
+ date = response.headers.get("DATE")
+ print(f"{date}:{response.url} with status {status}")
+ data = {'url': url, 'response': await response.text()}
+ return data
+
+
+async def bound_fetch(sem, url, session):
+ # Getter function with semaphore.
+ async with sem:
+ return await fetch(url, session)
+
+
+# def get_tenders_from_url1():
+# urls = get_urls()
+# tenders_id = []
+# for url in urls:
+# response = requests.get(url["url"])
+# soup = BeautifulSoup(response.content, "html.parser")
+# for tender in soup.find_all("td", class_="tender__id"):
+# id_tender = tender.text
+# print(id_tender + str(url["article"]))
+# tenders_id.append({"article": url["article"], "id_tender": id_tender, "url_tender": f"https://www.tender.pro/api/tender/{id_tender}/view_public"})
+# return tenders_id
+
+
+async def get_tenders_from_url(tender_state = 1):
+ urls = get_urls(tender_state)
+ return await search_tenders(urls)
+
+async def get_tenders_from_article(article):
+ urls = get_urls(article = article)
+ return await search_tenders(urls)
+
+
+def sooup(soup, tenders_id, res):
+ for tender in soup.find_all("tr", class_="table-stat__row"):
+ try:
+ id_tender = tender.find("td", class_="tender__id").text
+ date_tender = tender.find("td", class_="tender__untill").text
+ except Exception:
+ continue
+ # if id_tender == None:
+ # continue
+ # id_tender = id_tender.text
+ for id in tenders_id:
+ if id_tender in id["id_tender"]:
+ print("ПОВТОРЕНИЕ")
+ return tenders_id
+ print(id_tender, date_tender)
+ # resp = requests.get(f"http://www.tender.pro/api/_tender.item.json?_key=1732ede4de680a0c93d81f01d7bac7d1&company_id=1&id={id_tender}")
+ # try:
+ # goods = resp.json().get("result").get("data")
+ # goods_name = ""
+ # goods_amount = 0
+ # for g in goods:
+ # name = g.get("name")
+ # amount = float(g.get("amount"))
+ # if res['url']['article'].split("/")[1] in name:
+ # goods_name += name + " "
+ # goods_amount += amount
+ tenders_id.append({
+ "article": res['url']['article'],
+ "id_tender": id_tender,
+ "date_until": date_tender,
+ "url_tender": f"https://www.tender.pro/api/tender/{id_tender}/view_public",
+ # "goods_name": goods_name,
+ # "goods_amount": goods_amount,
+ })
+ # except Exception as e:
+ # print(e)
+ # pass
+ return tenders_id
+
+
+async def search_tenders(urls):
+ tasks = []
+ # create instance of Semaphore
+ sem = asyncio.Semaphore(5)
+ results = []
+ t = time()
+ # Create client session that will ensure we dont open new connection
+ # per each request.
+ async with ClientSession() as session:
+ for url in urls:
+ # pass Semaphore and session to every GET request
+ task = asyncio.ensure_future(bound_fetch(sem, url, session))
+ tasks.append(task)
+
+ responses = asyncio.gather(*tasks)
+ results = await responses
+ print(time()-t)
+ print(len(results))
+ t1 = time()
+ tenders_id = []
+ for res in results:
+ soup = BeautifulSoup(res["response"], "html.parser")
+ pag = soup.find("div", class_="pagination-pages")
+ print(f"pag: {pag}")
+ if (pag != None):
+ print(f"pag: {str(pag)[44:45]}")
+ pages = int(str(pag)[44:45])
+ urls2 = []
+ for i in range(pages):
+ urls2.append(f"{res['url']['url']}&page={i}")
+ print(urls2)
+ for ur in urls2:
+ response = requests.get(ur)
+ soup1 = BeautifulSoup(response.content, "html.parser")
+ tenders_id = sooup(soup1, tenders_id, res)
+ tenders_id = sooup(soup, tenders_id, res)
+
+ print(time() - t1)
+ for tend in tenders_id:
+ print(tend)
+
+ return tenders_id
+
+def get_excel_from_tenders(tenders_id, link = 'tgbot/data/tenders_id_all.xlsx'):
+ tends = pd.DataFrame(tenders_id)
+ tends.to_excel(link)
+
+
+
+# https://www.tender.pro/api/tender/876455/view_public
+
+# urls = get_urls1(article)
+# tenders_id = get_tenders_from_url(urls)
+# for tend in tenders_id:
+# print(tend)
+
+
+
+# zik@MacBook-Air-Ila парсер % /usr/local/bin/python3 /Users/zik/Documents/Programs/парсер/parser_tendors.py
+# 876455F-714117/612630080087
+# 878638F-742003/PL420
+# {'article': 'F-714117/612630080087', 'id_tender': '876455'}
+# {'article': 'F-742003/PL420', 'id_tender': '878638'}
+
+
+# http://www2.tender.pro/api/tenders/list?sid=15932209&company_id=415538&face_id=440662&order=3&tmpl-opts=%22company_id%3A415538%22%2C%22face_id%3A440662%22%2C%22order%3A3%22%2C%22view_tenders_list-tmpl-signup%3A1%22%2C%22filter_reset%3A1%22%2C%22view_tenders_list-tmpl-name%3A%22%2C%22view_tenders_list-tmpl-default%3A%22%2C%22tender_id%3A%22%2C%22tender_name%3A%22%2C%22company_name%3A%22%2C%22good_name%3ASFC-7939-30B%22%2C%22tender_type%3A100%22%2C%22tender_state%3A1%22%2C%22tender_interest_type%3A%22%2C%22tender_invited%3A%22%2C%22country%3A0%22%2C%22region%3A%22%2C%22basis%3A0%22%2C%22tender_show_own%3A0%22%2C%22okved%3A%22%2C%22dateb%3A%22%2C%22datee%3A%22%2C%22dateb2%3A%22%2C%22datee2%3A%22%2C%22by%3A25%22&view_tenders_list-tmpl-signup=1&filter_tmpl=0&filter_reset=1&view_tenders_list-tmpl-name=&view_tenders_list-tmpl-default=&tender_id=&tender_name=&company_name=&good_name=VG1540080032&tender_type=100&tender_state=1&tender_interest_type=&tender_invited=&country=0®ion=&basis=0&tender_show_own=0&okved=&dateb=&datee=&dateb2=&datee2=&by=25
+# http://www.tender.pro/api/_tender.info.json?_key=1732ede4de680a0c93d81f01d7bac7d1&company_id=44441&id=144276
+
+# https://www.tender.pro/api/tenders/list?sid=
+# &company_id=
+# &face_id=0
+# &order=3
+# &tender_id=
+# &tender_name=
+# &company_name=
+# &good_name=PL+420
+# &tender_type=100
+# &tender_state=100
+# &country=0
+# ®ion=
+# &basis=0
+# &okved=
+# &dateb=&datee=&dateb2=&datee2=
+
+# PL420
+
+# https://www.tender.pro/api/tenders/list?&good_name=PL420&tender_state=100
diff --git a/tgbot/services/tender_plan.py b/tgbot/services/tender_plan.py
new file mode 100644
index 0000000..21f6eb7
--- /dev/null
+++ b/tgbot/services/tender_plan.py
@@ -0,0 +1,588 @@
+import os
+import glob
+import requests
+import json
+import pandas as pd
+import re
+from thefuzz import fuzz # type: ignore
+from bs4 import BeautifulSoup
+from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
+from datetime import datetime
+import time
+import asyncio
+from aiohttp import ClientSession
+
+from tgbot.utils.misc.bot_logging import bot_logger
+from tgbot.services.parser_tendors import get_articles
+
+
+cookies = {
+ 'jwt': 's%3ABearer%200b998917d77807264c0b82d9ff64ca6fcefb01437019151eff52a4e706ebcd405099a030761ba0f5c6574aea12c525293c7da298b267c462224502a8c88c5289.p%2FACkoaEjBIK4u5vArNpU3Fh24DqBsyfGcHN8h%2BILig',
+ 'referer': 'https://tenderplan.ru/app',
+ 'source': 'response_type=code&client_id=619e606a7883684e0e3d10c7&redirect_uri=https%253A%252F%252Fbitrix24.tenderplan.ru%252Ftenderplan%252Foauth&scope=resources%253Aexternal%2520comments%2520marks%253Aread%2520notes%2520relations%253Aread%2520firm%253Aread&state=2f8e5161-d7e0-4e88-9e37-df3f1f75f20a',
+ '__ddg1_': 'ZKa7JlUseYuy3cvawO9W',
+}
+
+headers = {
+ 'Accept': '*/*',
+ 'Authorization': 'Bearer 0b998917d77807264c0b82d9ff64ca6fcefb01437019151eff52a4e706ebcd405099a030761ba0f5c6574aea12c525293c7da298b267c462224502a8c88c5289',
+ 'Sec-Fetch-Site': 'same-origin',
+ 'Accept-Language': 'ru',
+ # 'Accept-Encoding': 'gzip, deflate, br',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Host': 'tenderplan.ru',
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15',
+ 'Connection': 'keep-alive',
+ 'Referer': 'https://tenderplan.ru/app',
+ # 'Cookie': 'jwt=s%3ABearer%200b998917d77807264c0b82d9ff64ca6fcefb01437019151eff52a4e706ebcd405099a030761ba0f5c6574aea12c525293c7da298b267c462224502a8c88c5289.p%2FACkoaEjBIK4u5vArNpU3Fh24DqBsyfGcHN8h%2BILig; referer=https://tenderplan.ru/app; source=response_type=code&client_id=619e606a7883684e0e3d10c7&redirect_uri=https%253A%252F%252Fbitrix24.tenderplan.ru%252Ftenderplan%252Foauth&scope=resources%253Aexternal%2520comments%2520marks%253Aread%2520notes%2520relations%253Aread%2520firm%253Aread&state=2f8e5161-d7e0-4e88-9e37-df3f1f75f20a; __ddg1_=ZKa7JlUseYuy3cvawO9W',
+ 'Sec-Fetch-Dest': 'empty',
+}
+
+# def search_in_tenderplan():
+ # https://tenderplan.ru/api/tenders/getlist?q=F-714117
+ # https://tenderplan.ru/api/tenders/getlist?page=1&q=Komatsu
+ # ...
+
+
+def get_urls(article = 0):
+ urls = []
+ if (article == 0):
+ article = get_articles()
+ for val in article.iloc:
+ # print(val["Артикул"])
+ for art in val["Артикул"]:
+ urls.append({"article": f"{val['Наименование']} / {art}", "art": art, "url": f"https://tenderplan.ru/api/tenders/getlist?isActual=1&q={art}"})
+ else:
+ articles = article.split(", |,")
+ for art in articles:
+ urls.append({"article": f"{art}", "url": f"https://tenderplan.ru/api/tenders/getlist?isActual=1&q={art}"})
+
+ return urls
+
+
+# async def fetch(url, session):
+# st = ''
+# try:
+# async with session.get(url['url']) as response:
+# k = 0
+# st = response.status
+# while response.status != 200:
+# time.sleep(5)
+# print('sleeep')
+# k += 1
+# if k > 5: break
+
+# date = response.headers.get("DATE")
+# print(f"{date}:{response.url} with status {response.status}")
+# data = {'url': url, 'response': await response.json()}
+# return data
+# except Exception as e:
+# print(e)
+# bot_logger.error(f"st:{st} --- {e}")
+
+
+# async def bound_fetch(sem, url, session):
+# # Getter function with semaphore.
+# async with sem:
+# return await fetch(url, session)
+
+async def fetch(url, session, retry_event):
+ st = ''
+ try:
+ async with session.get(url['url']) as response:
+ k = 0
+ st = response.status
+ while response.status == 429:
+ await retry_event.wait() # Ожидаем разрешения продолжения запросов
+ async with session.get(url['url']) as response:
+ st = response.status
+
+ while response.status != 200 and k <= 5:
+ await asyncio.sleep(5) # Асинхронная задержка
+ print('sleeep')
+ k += 1
+ async with session.get(url['url']) as response:
+ st = response.status
+
+ if response.status == 200:
+ date = response.headers.get("DATE")
+ print(f"{date}: {response.url} со статусом {response.status}")
+ data = {'url': url, 'response': await response.json()}
+ return data
+ else:
+ print(f"Не удалось получить {url['url']} после {k} попыток")
+ return None
+ except Exception as e:
+ print(e)
+ bot_logger.error(f"st: {st} --- {e}")
+
+async def bound_fetch(sem, url, session, retry_event):
+ # Функция получения данных с семафором
+ async with sem:
+ return await fetch(url, session, retry_event)
+
+
+
+# async def get_tenders_from_url(tender_state = 1):
+# urls = get_urls(tender_state)
+# return await search_tenders(urls)
+
+# async def get_tenders_from_article(article):
+# urls = get_urls(article = article)
+# return await search_tenders(urls)
+
+def sooup(tenders_id, tenders, res):
+ for tender in tenders:
+ tend_name = tender.get('orderName')
+ tend_id = tender.get('_id')
+ good_count = ''
+ submissionCloseDateTime = tender.get('submissionCloseDateTime')
+ date_until = datetime.fromtimestamp(submissionCloseDateTime/1000).strftime('%Y-%m-%d')
+
+ params = {'id': tend_id,}
+ response = requests.get('https://tenderplan.ru/api/tenders/get', params=params, cookies=cookies, headers=headers)
+ if "Количество" in json.loads(response.json().get('json')).get("0").get("fv").get("0").get("fv").get("th").get("1").get("fv"):
+ goods = json.loads(response.json().get('json')).get("0").get("fv").get("0").get("fv").get("tb")
+ for good in goods:
+ good_name = goods.get(good).get('0').get('fv')
+ if res.get('url').get('art') in good_name:
+ good_count = goods.get(good).get('1').get('fv')
+
+ print(f"name - {good_name}")
+
+ print(tend_id, date_until)
+
+ # поиск цены в таблицах
+ price = []
+ path = "tgbot/data/"
+ abs_path = os.path.abspath(path)
+ # Поиск и вывод файлов
+ excel_files = [name for name in glob.glob(f'{abs_path}/price*.xls*')]
+ # print(f'excel_files-{excel_files}')
+ for file in excel_files:
+ # print(f'file-{file}')
+ # Загрузка Excel файла
+ excel_file = file
+ df = pd.read_excel(excel_file)
+ search_term = res.get("url").get("art")
+ if search_term.isalpha():
+ continue
+ # Поиск строк, содержащих текст запроса
+ sear = df[df.apply(lambda row: row.astype(str).str.contains(search_term, case=False).any(), axis=1)].to_dict('index')
+ print("sear ",sear)
+ price.append({file.split('/')[-1]: sear})
+ print("price ",price)
+
+ # price - ['Артикул','Бренд','Кол-во','Цена, руб. с НДС']
+
+ for pri in price:
+ print("pri - ", sear)
+ for keyprice, valprice in pri.items():
+ for p in valprice.values():
+ print("p - ", p)
+ tenders_id.append({
+ "article": res.get('url').get('article'),
+ "art0": res.get('url').get('art'),
+ # "price": price,
+ "файл": keyprice,
+ "Артикул": p.get("Артикул"),
+ "Бренд": p.get("Бренд"),
+ "Кол-во": p.get("Кол-во"),
+ "Цена": p.get("Цена, руб. с НДС"),
+ "good_count": good_count,
+ "id_tender": tend_id,
+ "url_tender": f"https://tenderplan.ru/app?tender={tend_id}",
+ "date_until": date_until,
+ "tend_name": tend_name,
+ "platform": response.json().get('platform').get('name'),
+ "href": response.json().get('href'),
+ })
+ else:
+ tenders_id.append({
+ "article": res.get('url').get('article'),
+ "art0": res.get('url').get('art'),
+ # "price": price,
+ "файл": '--',
+ "Артикул": '--',
+ "Бренд": '--',
+ "Кол-во": '--',
+ "Цена": '--',
+ "good_count": good_count,
+ "id_tender": tend_id,
+ "url_tender": f"https://tenderplan.ru/app?tender={tend_id}",
+ "date_until": date_until,
+ "tend_name": tend_name,
+ "platform": response.json().get('platform').get('name'),
+ "href": response.json().get('href'),
+ })
+ # except Exception as e:
+ # print(e)
+ # pass
+ return tenders_id
+
+
+async def search_in_tenderplan(urls = 0):
+ tenders_id = []
+ try:
+ if urls == 0:
+ urls = get_urls()
+ else:
+ return 0
+ tasks = []
+ # create instance of Semaphore
+ sem = asyncio.Semaphore(3)
+ retry_event = asyncio.Event()
+ retry_event.set() # Устанавливаем событие в начальное состояние
+
+ results = []
+ t = time.time()
+ # Create client session that will ensure we dont open new connection
+ # per each request.
+ async with ClientSession(cookies=cookies, headers=headers) as session:
+ tasks = [bound_fetch(sem, url, session, retry_event) for url in urls]
+
+ while True:
+ results1 = await asyncio.gather(*tasks, return_exceptions=True)
+ for result in results1:
+ if isinstance(result, Exception):
+ continue
+ if result and result.get('response').get('status') == 429:
+ retry_event.clear() # Останавливаем отправку запросов при 429
+ await asyncio.sleep(5) # Ждем некоторое время перед повторной попыткой
+ retry_event.set() # Разрешаем отправку запросов снова
+ if all(result and result['response']['status'] == 200 for result in results if not isinstance(result, Exception)):
+ break # Завершаем, если все запросы успешны
+
+ # responses = asyncio.gather(*tasks)
+ results = results1
+ print(time.time()-t)
+ print(len(results))
+ t1 = time.time()
+ for res in results:
+ try:
+ if res.get('response').get('tenders'):
+ tenders = res.get('response').get('tenders')
+ #pagination
+ # if len(tenders) > 50:
+ # page = 1
+ # urls2 = f"https://tenderplan.ru/api/tenders/getlist?page={page}&q={}"
+ # while len(tenders) > 50:
+ # response = requests.get(ur)
+ # soup1 = BeautifulSoup(response.json, "html.parser")
+ # tenders_id = sooup(soup1, tenders_id, res)
+ ########
+ tenders_id = sooup(tenders_id, tenders, res)
+ else:
+ print('tenders none')
+ # bot_logger.error(f"tenders none, скорее всего 429 ошибка")
+ # raise Exception("tenders none")
+ except Exception as e:
+ print(e)
+ bot_logger.error(f"{e}")
+
+ print(time.time() - t1)
+ for tend in tenders_id:
+ print(tend)
+
+ get_excel_from_tenderplan(tenders_id)
+
+ return tenders_id
+ except Exception as e:
+ print(e)
+ get_excel_from_tenderplan(tenders_id)
+ bot_logger.error(f"{e}")
+
+def get_excel_from_tenderplan(tenders_id, link = 'tgbot/data/tenders_tenderplan_from_art.xlsx'):
+ # oldtends = pd.DataFrame(link)
+ # newtends = pd.DataFrame(tenders_id)
+
+ # Предположим, ваш список с тендерами называется ``
+ df = pd.DataFrame(tenders_id)
+ df = df.drop_duplicates(ignore_index=True)
+
+ # Читаем существующий Excel файл, если он есть
+ try:
+ existing_df = pd.read_excel(link, engine='openpyxl')
+ except FileNotFoundError:
+ existing_df = pd.DataFrame()
+
+ # Объединяем DataFrame с помощью merge, используя indicator=True
+ merged_df = df.merge(existing_df, how='left', indicator=True)
+
+ # Определяем строки, которые есть в ОБЕИХ таблицах
+ in_both_df = merged_df[merged_df['_merge'] == 'both']
+
+ # Сохраняем в Excel с выделением нужных строк красным
+ writer = pd.ExcelWriter(link, engine='xlsxwriter')
+ df.to_excel(writer, sheet_name='Tenders', index=False)
+
+ # Получаем объект workbook
+ workbook = writer.book
+ # Получаем объект worksheet
+ worksheet = workbook.get_worksheet_by_name('Tenders')
+
+ # Применяем стиль к строкам, которые есть в ОБЕИХ таблицах
+ for index in in_both_df.index:
+ worksheet.conditional_format(index+1, 0, index+1, len(df.columns)-1, {
+ 'type': 'no_errors',
+ 'format': workbook.add_format({'bg_color': '#FFC7CE'})
+ })
+
+ # Сохраняем файл
+ writer.close()
+
+
+
+ # newtends.to_excel(link)
+
+
+
+######################################--------AUTOPITER-------########################################
+
+
+def split_search(search_string):
+ search_split = []
+ digit_count = sum(char.isdigit() for char in search_string)
+ if digit_count <= 1:
+ return []
+ for word in search_string.split(" "):
+ digit_count = sum(char.isdigit() for char in word)
+ if (
+ digit_count >= len(word) // 4
+ and len(word)>=6
+ and not(bool(re.search('[а-яА-Я]', word)))
+
+ ):
+ search_split.append(word)
+ # if bool(re.search(r'\d', good_name)):
+ # continue
+ return search_split
+
+
+def search_in_autopiter(search: str):
+
+
+ params = {
+ 'detailNumber': search,
+ 'isFullQuery': 'true',
+ }
+
+ try:
+ ap_search = search
+ resp = requests.get('https://autopiter.ru/api/api/searchdetails', params=params)
+ print("response - ", resp.status_code)
+ print("good search - ", ap_search)
+ goodauto = []
+
+ if resp.status_code == 200:
+ params = {'idArticles': [position.get('id') for position in resp.json().get('data').get('catalogs')]}
+ get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params)
+ print(get_cost_resp)
+
+ for position in resp.json().get('data').get('catalogs'):
+ # print("position ",position)
+ ap_name = position.get('name')
+ ap_id = position.get('id')
+ ap_number = position.get('number')
+ if get_cost_resp.status_code == 200:
+ ap_originalPrice = [cost.get('originalPrice') for cost in get_cost_resp.json().get('data') if cost.get('id') == ap_id and cost.get('originalPrice') > 0]
+ if ap_originalPrice == []: continue
+ else: ap_originalPrice = ap_originalPrice[0]
+ else:
+ ap_originalPrice = "--"
+ # print("ap_originalPrice - ",ap_originalPrice)
+ # print("descr совпадение - ",fuzz.partial_token_sort_ratio(name, descr))
+ goodauto.append({
+ 'ap_search': ap_search,
+ 'ap_name': ILLEGAL_CHARACTERS_RE.sub(r'', ap_name),
+ 'fuzz': fuzz.partial_token_sort_ratio(ap_search, ap_name),
+ 'ap_number': ap_number,
+ 'ap_originalPrice': ap_originalPrice,
+ 'link_autopiter': f"https://autopiter.ru/goods/{ap_number}/{position.get('catalogUrl')}/id{ap_id}",
+ 'ap_id': ap_id,
+ })
+ elif resp.status_code == 429:
+ raise Exception("429")
+ if goodauto != []:
+ goodauto = pd.DataFrame(goodauto).sort_values(by="fuzz",ascending=False)[:1]
+ elif goodauto == []:
+ goodauto = [{
+ 'ap_search': '----',
+ 'ap_name': '----',
+ 'fuzz': '',
+ 'ap_number': '',
+ 'ap_originalPrice': '',
+ 'link_autopiter': '----',
+ }]
+ return goodauto
+ # goodsinauto.append(goodsinauto)
+
+ # print("name - ",search_in_autopiter(name))
+ # amount = float(g.get("amount"))
+ # if tend.get('article').split("/")[1] in name:
+ # goods_name += name + " "
+ # goods_amount += amount
+ except Exception as e:
+ print(e)
+ bot_logger.error(f"{e}")
+ return []
+
+
+def tenders_with_goods(pagecount: int = 1):
+ try:
+ tenders_with_goods = []
+ count = 0
+ page = 0
+ countn = 50
+ while True:
+ try:
+ params = {'page': page}
+ response = requests.get('https://tenderplan.ru/api/tenders/getlist', params=params, cookies=cookies, headers=headers)
+ # if (response.json().get('tenders') == []):
+ # break
+ if (page>=pagecount):
+ break
+ # if (count>countn):
+ # break
+ page += 1
+ tenders = response.json().get('tenders')
+ print(f"--------{len(tenders)}")
+ for tend in tenders:
+ try:
+ tend_name = tend.get('orderName')
+ id = tend.get('_id')
+ print(f"tender -- {tend_name} -- {id}")
+ params = {
+ 'id': id,
+ }
+ response = requests.get('https://tenderplan.ru/api/tenders/get', params=params, cookies=cookies, headers=headers)
+ if "ObjectInfo" in response.json().get('json'):
+ goods = json.loads(response.json().get('json'))["0"]["fv"]["0"]["fv"]["tb"]
+ submission_close_timestamp = int(json.loads(response.json().get('json'))['1']['fv']['1']['fv'])
+ print(submission_close_timestamp)
+ submission_close_datetime = datetime.fromtimestamp(submission_close_timestamp/1000).strftime('%Y-%m-%d %H:%M:%S')
+ for good in goods:
+ # if (count>countn):
+ # break
+ good_name = goods.get(good).get('0').get('fv')
+ print(f"name - {good_name}")
+ # count += 1
+
+ for name in split_search(good_name):
+ ap_search = search_in_autopiter(name)
+ for ap_s in ap_search.iterrows():
+ # print(ap_s)
+ count += 1
+ print("count",count)
+ tenders_with_goods.append({
+ "tend_name": tend_name,
+ "tend_link": f"https://tenderplan.ru/app?tender={id}",
+ "tend_under": submission_close_datetime,
+ "good_name": good_name,
+ "ap_good_name": name,
+ "ap_search_name": ap_s[1].get('ap_name'),
+ "ap_search_fuzz": ap_s[1].get('fuzz'),
+ "ap_search_link": ap_s[1].get('link_autopiter'),
+ "ap_id": ap_s[1].get('ap_id'),
+ "ap_search_price": ap_s[1].get('ap_originalPrice'),
+ })
+ # break
+ except Exception as e1:
+ print("error -- ",e1)
+ bot_logger.error(f"{e1}")
+ except Exception as e:
+ print(e)
+ bot_logger.error(f"{e}")
+ print(f"count - {count}")
+
+ # for twg in tenders_with_goods:
+ # print(twg)
+
+ print(f"excel!!!!!!!!!")
+ tenders_with_goods = get_all_price(tenders_with_goods)
+ tends = pd.DataFrame(tenders_with_goods)
+ tends.to_excel(r'tgbot/data/tenders_with_goods.xlsx')
+ print(f"excel!!!!!!!!!2")
+
+
+ return tenders_with_goods
+ except Exception as e:
+ print(e)
+ bot_logger.error(f"{e}")
+
+
+def get_all_price(tenders_with_goods):
+ ap_ids = []
+ c = 0
+ tenders_with_goods2 = []
+ for ap in tenders_with_goods:
+ ap_ids.append(ap.get('ap_id'))
+
+ params = {'idArticles': ap_ids}
+ get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params)
+ while get_cost_resp.status_code == 429:
+ time.sleep(5)
+ c += 1
+ get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params)
+ if c>10: break
+
+ print(get_cost_resp.status_code)
+ cost_resp = get_cost_resp.json().get('data')
+ for ap in tenders_with_goods:
+ try:
+ id = ap.get("ap_id")
+ cost = [cost.get('originalPrice') for cost in cost_resp if cost.get('id') == id]
+
+ if cost:
+ ap['ap_search_price'] = cost[0]
+ tenders_with_goods2.append(ap)
+ except Exception as e:
+ print(e)
+ bot_logger.error(f"{e}")
+ return tenders_with_goods2
+
+
+def beautitext(text: str):
+
+ # texts = re.findall(r'\w', text)
+
+ tt = []
+
+ texts = text.split(" ")
+ for te in texts:
+ digit_count = sum(char.isdigit() for char in te)
+ if digit_count >= len(te) // 2:
+ tt.append(te)
+ # if bool(re.search(r'\d', te)):
+ # tt.append(te)
+
+
+ return str(tt)
+
+# print(beautitext('Помпа ЗМЗ-406, 409 /Евро-3, Евро-4'))
+# tenders_with_goods()
+
+
+
+# print(search_in_autopiter('Форсунка топливная Common Rail КАМАЗ'))
+
+
+
+
+
+
+# curl 'https://tenderplan.ru/api/tenders/getlist?' \
+# -X 'GET' \
+# -H 'Accept: */*' \
+# -H 'Authorization: Bearer f7dcac67acdb2a348f5c81fc26cfafaba892a7bc02dceb97ffe079ad60b0edb4399522590c067b24459b785fa019006943700cc47033ca26b7aecd22f3777077' \
+# -H 'Sec-Fetch-Site: same-origin' \
+# -H 'Accept-Language: ru' \
+# -H 'Accept-Encoding: gzip, deflate, br' \
+# -H 'Sec-Fetch-Mode: cors' \
+# -H 'Host: tenderplan.ru' \
+# -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15' \
+# -H 'Referer: https://tenderplan.ru/app?key=0&tender=6628ae9952e24fc13583dd05' \
+# -H 'Connection: keep-alive' \
+# -H 'Cookie: jwt=s%3ABearer%20f7dcac67acdb2a348f5c81fc26cfafaba892a7bc02dceb97ffe079ad60b0edb4399522590c067b24459b785fa019006943700cc47033ca26b7aecd22f3777077.UH%2BcCzOylTzLr%2BF6Hf4kerem6GuMoVK%2FBSiOYmPCkEc; source=key=0&tender=6628ae9952e24fc13583dd05; previousUrl=tenderplan.ru%2Fbitrix24%2Finstructions%2F; tildasid=1713941269630.966260; tildauid=1713888711831.359844; referer=https://tenderplan.ru/app; __ddg1_=ZKa7JlUseYuy3cvawO9W' \
+# -H 'Sec-Fetch-Dest: empty' \
+# -H 'Socket: ZTvzcuitnjsM-m4OBCP4'
\ No newline at end of file
diff --git a/tgbot/services/test.py b/tgbot/services/test.py
new file mode 100644
index 0000000..ad843f7
--- /dev/null
+++ b/tgbot/services/test.py
@@ -0,0 +1,163 @@
+import requests
+import json
+import pandas as pd
+import re
+from thefuzz import fuzz, process
+from bs4 import BeautifulSoup
+from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
+from datetime import datetime
+import time
+
+from tgbot.utils.misc.bot_logging import bot_logger
+
+cookies = {
+ 'jwt': 's%3ABearer%20...',
+ 'referer': 'https://tenderplan.ru/app?key=0&tender=6639e01152e24fc13574139f',
+ 'source': 'key=0&tender=6639e01152e24fc13574139f',
+ 'tildauid': '1713888711831.359844',
+ '__ddg1_': 'ZKa7JlUseYuy3cvawO9W',
+}
+
+headers = {
+ 'Accept': '*/*',
+ 'Authorization': 'Bearer a4dc57cc44d5ca62a06a7b19660840a66f3048028b417bbc813a1acf6f3691da841b9120373431377409359f64430b0644ee22ddf072fbe6ad656b57eeebe83d',
+ 'Sec-Fetch-Site': 'same-origin',
+ 'Accept-Language': 'ru',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Host': 'tenderplan.ru',
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15',
+ 'Connection': 'keep-alive',
+ 'Referer': 'https://tenderplan.ru/app?key=0&tender=6639e01152e24fc13574139f',
+ 'Sec-Fetch-Dest': 'empty',
+}
+
+def clean_text(text):
+ return re.sub(r'[^\w\s]', '', text).lower()
+
+def split_search(search_string):
+ search_split = []
+ digit_count = sum(char.isdigit() for char in search_string)
+ if digit_count <= 1:
+ return []
+ for word in search_string.split(" "):
+ digit_count = sum(char.isdigit() for char in word)
+ if digit_count >= len(word) // 4 and len(word) >= 6 and not re.search('[а-яА-Я]', word):
+ search_split.append(word)
+ return search_split
+
+def search_in_autopiter(search):
+ params = {'detailNumber': search, 'isFullQuery': 'true'}
+ try:
+ resp = requests.get('https://autopiter.ru/api/api/searchdetails', params=params)
+ goodauto = []
+
+ if resp.status_code == 200:
+ params = {'idArticles': [position.get('id') for position in resp.json().get('data').get('catalogs')]}
+ get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params)
+
+ for position in resp.json().get('data').get('catalogs'):
+ ap_name = position.get('name')
+ ap_id = position.get('id')
+ ap_number = position.get('number')
+ if get_cost_resp.status_code == 200:
+ ap_originalPrice = next((cost.get('originalPrice') for cost in get_cost_resp.json().get('data') if cost.get('id') == ap_id and cost.get('originalPrice') > 0), "--")
+ else:
+ ap_originalPrice = "--"
+ goodauto.append({
+ 'ap_search': search,
+ 'ap_name': ILLEGAL_CHARACTERS_RE.sub(r'', ap_name),
+ 'fuzz': fuzz.partial_token_sort_ratio(search, ap_name),
+ 'ap_number': ap_number,
+ 'ap_originalPrice': ap_originalPrice,
+ 'link_autopiter': f"https://autopiter.ru/goods/{ap_number}/{position.get('catalogUrl')}/id{ap_id}",
+ 'ap_id': ap_id,
+ })
+ elif resp.status_code == 429:
+ raise Exception("429")
+ if goodauto:
+ goodauto = pd.DataFrame(goodauto).sort_values(by="fuzz", ascending=False)[:1]
+ else:
+ goodauto = [{
+ 'ap_search': '----',
+ 'ap_name': '----',
+ 'fuzz': '',
+ 'ap_number': '',
+ 'ap_originalPrice': '',
+ 'link_autopiter': '----',
+ }]
+ return goodauto
+ except Exception as e:
+ bot_logger.error(f"{e}")
+ return []
+
+def tenders_with_goods(pagecount=1):
+ try:
+ tenders_with_goods = []
+ page = 0
+
+ while page < pagecount:
+ params = {'page': page}
+ response = requests.get('https://tenderplan.ru/api/tenders/getlist', params=params, cookies=cookies, headers=headers)
+ tenders = response.json().get('tenders')
+ page += 1
+
+ for tend in tenders:
+ tend_name = tend.get('orderName')
+ id = tend.get('_id')
+ params = {'id': id}
+ response = requests.get('https://tenderplan.ru/api/tenders/get', params=params, cookies=cookies, headers=headers)
+ if "ObjectInfo" in response.json().get('json'):
+ goods = json.loads(response.json().get('json'))["0"]["fv"]["0"]["fv"]["tb"]
+ submission_close_timestamp = int(json.loads(response.json().get('json'))['1']['fv']['1']['fv'])
+ submission_close_datetime = datetime.fromtimestamp(submission_close_timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S')
+
+ for good in goods.values():
+ good_name = good.get('0').get('fv')
+ for name in split_search(good_name):
+ ap_search = search_in_autopiter(name)
+ for ap_s in ap_search.iterrows():
+ tenders_with_goods.append({
+ "tend_name": tend_name,
+ "tend_link": f"https://tenderplan.ru/app?tender={id}",
+ "tend_under": submission_close_datetime,
+ "good_name": good_name,
+ "ap_good_name": name,
+ "ap_search_name": ap_s[1].get('ap_name'),
+ "ap_search_fuzz": ap_s[1].get('fuzz'),
+ "ap_search_link": ap_s[1].get('link_autopiter'),
+ "ap_id": ap_s[1].get('ap_id'),
+ "ap_search_price": ap_s[1].get('ap_originalPrice'),
+ })
+ tenders_with_goods = get_all_price(tenders_with_goods)
+ tends = pd.DataFrame(tenders_with_goods)
+ tends.to_excel('tgbot/data/tenders_with_goods.xlsx')
+
+ return tenders_with_goods
+ except Exception as e:
+ bot_logger.error(f"{e}")
+
+def get_all_price(tenders_with_goods):
+ ap_ids = [ap.get('ap_id') for ap in tenders_with_goods]
+ params = {'idArticles': ap_ids}
+ get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params)
+
+ retry_count = 0
+ while get_cost_resp.status_code == 429 and retry_count < 10:
+ time.sleep(5)
+ retry_count += 1
+ get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params)
+
+ if get_cost_resp.status_code == 200:
+ cost_resp = get_cost_resp.json().get('data')
+ for ap in tenders_with_goods:
+ id = ap.get("ap_id")
+ cost = next((cost.get('originalPrice') for cost in cost_resp if cost.get('id') == id), None)
+ if cost:
+ ap['ap_search_price'] = cost
+
+ return tenders_with_goods
+
+def beautitext(text):
+ texts = text.split(" ")
+ tt = [te for te in texts if sum(char.isdigit() for char in te) >= len(te) // 2]
+ return str(tt)
diff --git a/tgbot/utils/.DS_Store b/tgbot/utils/.DS_Store
new file mode 100644
index 0000000..18b6f56
Binary files /dev/null and b/tgbot/utils/.DS_Store differ
diff --git a/tgbot/utils/__init__.py b/tgbot/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/utils/const_functions.py b/tgbot/utils/const_functions.py
new file mode 100644
index 0000000..3ff05d3
--- /dev/null
+++ b/tgbot/utils/const_functions.py
@@ -0,0 +1,387 @@
+# - *- coding: utf- 8 - *-
+import random
+import time
+import uuid
+from datetime import datetime
+from typing import Union
+
+import pytz
+from aiogram import Bot
+from aiogram.types import InlineKeyboardButton, KeyboardButton, WebAppInfo, Message, InlineKeyboardMarkup, \
+ ReplyKeyboardMarkup
+
+from tgbot.utils.misc.bot_logging import bot_logger
+from tgbot.data.config import get_admins, BOT_TIMEZONE
+
+######################################## AIOGRAM ########################################
+# Генерация реплай кнопки
+def rkb(text: str) -> KeyboardButton:
+ return KeyboardButton(text=text)
+
+
+# Генерация инлайн кнопки
+def ikb(text: str, data: str = None, url: str = None, switch: str = None, web: str = None) -> InlineKeyboardButton:
+ if data is not None:
+ return InlineKeyboardButton(text=text, callback_data=data)
+ elif url is not None:
+ return InlineKeyboardButton(text=text, url=url)
+ elif switch is not None:
+ return InlineKeyboardButton(text=text, switch_inline_query=switch)
+ elif web is not None:
+ return InlineKeyboardButton(text=text, web_app=WebAppInfo(url=web))
+
+
+# Удаление сообщения с обработкой ошибки от телеграма
+async def del_message(message: Message):
+ try:
+ await message.delete()
+ except:
+ ...
+
+
+# Умная отправка сообщений (автоотправка сообщения с фото или без)
+async def smart_message(
+ bot: Bot,
+ user_id: int,
+ text: str,
+ keyboard: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] = None,
+ photo: Union[str, None] = None,
+):
+ if photo is not None and photo.title() != "None":
+ await bot.send_photo(
+ chat_id=user_id,
+ photo=photo,
+ caption=text,
+ reply_markup=keyboard,
+ )
+ else:
+ await bot.send_message(
+ chat_id=user_id,
+ text=text,
+ reply_markup=keyboard,
+ )
+
+
+# Отправка сообщения всем админам
+async def send_admins(bot: Bot, text: str, markup=None, not_me=0):
+ for admin in get_admins():
+ try:
+ if str(admin) != str(not_me):
+ await bot.send_message(
+ admin,
+ text,
+ reply_markup=markup,
+ disable_web_page_preview=True,
+ )
+ except:
+ ...
+
+# # Отправка сообщения пользователям по тендорам
+# async def send_employees(bot: Bot, text: str, markup=None, not_me=0):
+# get_users = get_employees()
+# bot_logger.warning("employee {get_users}")
+
+
+# # for admin in get_admins():
+# # try:
+# # if str(admin) != str(not_me):
+# # await bot.send_message(
+# # admin,
+# # text,
+# # reply_markup=markup,
+# # disable_web_page_preview=True,
+# # )
+# # except:
+# # ...
+
+######################################## ПРОЧЕЕ ########################################
+# Удаление отступов в многострочной строке ("""text""")
+def ded(get_text: str) -> str:
+ if get_text is not None:
+ split_text = get_text.split("\n")
+ if split_text[0] == "": split_text.pop(0)
+ if split_text[-1] == "": split_text.pop()
+ save_text = []
+
+ for text in split_text:
+ while text.startswith(" "):
+ text = text[1:].strip()
+
+ save_text.append(text)
+ get_text = "\n".join(save_text)
+ else:
+ get_text = ""
+
+ return get_text
+
+
+# Очистка текста от HTML тэгов ('test' -> *b*test*/b*)
+def clear_html(get_text: str) -> str:
+ if get_text is not None:
+ if "" in get_text: get_text = get_text.replace("<", "*")
+ if "<" in get_text: get_text = get_text.replace("<", "*")
+ if ">" in get_text: get_text = get_text.replace(">", "*")
+ else:
+ get_text = ""
+
+ return get_text
+
+
+# Очистка пробелов в списке (['', 1, ' ', 2] -> [1, 2])
+def clear_list(get_list: list) -> list:
+ while "" in get_list: get_list.remove("")
+ while " " in get_list: get_list.remove(" ")
+ while "." in get_list: get_list.remove(".")
+ while "," in get_list: get_list.remove(",")
+ while "\r" in get_list: get_list.remove("\r")
+ while "\n" in get_list: get_list.remove("\n")
+
+ return get_list
+
+
+# Разбив списка на несколько частей ([1, 2, 3, 4] 2 -> [[1, 2], [3, 4]])
+def split_list(get_list: list, count: int) -> list[list]:
+ return [get_list[i:i + count] for i in range(0, len(get_list), count)]
+
+
+# Получение текущей даты (True - дата с временем, False - дата без времени)
+def get_date(full: bool = True) -> str:
+ if full:
+ return datetime.now(pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M:%S")
+ else:
+ return datetime.now(pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y")
+
+
+# Получение текущего unix времени (True - время в наносекундах, False - время в секундах)
+def get_unix(full: bool = False) -> int:
+ if full:
+ return time.time_ns()
+ else:
+ return int(time.time())
+
+
+# Конвертация unix в дату и даты в unix
+def convert_date(from_time, full=True, second=True) -> Union[str, int]:
+ from tgbot.data.config import BOT_TIMEZONE
+
+ if "-" in str(from_time):
+ from_time = from_time.replace("-", ".")
+
+ if str(from_time).isdigit():
+ if full:
+ to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M:%S")
+ elif second:
+ to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M")
+ else:
+ to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y")
+ else:
+ if " " in str(from_time):
+ cache_time = from_time.split(" ")
+
+ if ":" in cache_time[0]:
+ cache_date = cache_time[1].split(".")
+ cache_time = cache_time[0].split(":")
+ else:
+ cache_date = cache_time[0].split(".")
+ cache_time = cache_time[1].split(":")
+
+ if len(cache_date[0]) == 4:
+ x_year, x_month, x_day = cache_date[0], cache_date[1], cache_date[2]
+ else:
+ x_year, x_month, x_day = cache_date[2], cache_date[1], cache_date[0]
+
+ x_hour, x_minute, x_second = cache_time[0], cache_time[2], cache_time[2]
+
+ from_time = f"{x_day}.{x_month}.{x_year} {x_hour}:{x_minute}:{x_second}"
+ else:
+ cache_date = from_time.split(".")
+
+ if len(cache_date[0]) == 4:
+ x_year, x_month, x_day = cache_date[0], cache_date[1], cache_date[2]
+ else:
+ x_year, x_month, x_day = cache_date[2], cache_date[1], cache_date[0]
+
+ from_time = f"{x_day}.{x_month}.{x_year}"
+
+ if " " in str(from_time):
+ to_time = int(datetime.strptime(from_time, "%d.%m.%Y %H:%M:%S").timestamp())
+ else:
+ to_time = int(datetime.strptime(from_time, "%d.%m.%Y").timestamp())
+
+ return to_time
+
+
+# Генерация уникального айди
+def gen_id() -> int:
+ mac_address = uuid.getnode()
+ time_unix = int(str(time.time_ns())[:16])
+
+ return mac_address + time_unix
+
+
+# Генерация пароля | default, number, letter, onechar
+def gen_password(len_password: int = 16, type_password: str = "default") -> str:
+ if type_password == "default":
+ char_password = list("1234567890abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ")
+ elif type_password == "letter":
+ char_password = list("abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ")
+ elif type_password == "number":
+ char_password = list("1234567890")
+ elif type_password == "onechar":
+ char_password = list("1234567890")
+
+ random.shuffle(char_password)
+ random_chars = "".join([random.choice(char_password) for x in range(len_password)])
+
+ if type_password == "onechar":
+ random_chars = f"{random.choice('abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ')}{random_chars[1:]}"
+
+ return random_chars
+
+
+# Дополнение к числу корректного времени (1 -> 1 день, 3 -> 3 дня)
+def convert_times(get_time: int, get_type: str = "day") -> str:
+ get_time = int(get_time)
+ if get_time < 0: get_time = 0
+
+ if get_type == "second":
+ get_list = ['секунда', 'секунды', 'секунд']
+ elif get_type == "minute":
+ get_list = ['минута', 'минуты', 'минут']
+ elif get_type == "hour":
+ get_list = ['час', 'часа', 'часов']
+ elif get_type == "day":
+ get_list = ['день', 'дня', 'дней']
+ elif get_type == "month":
+ get_list = ['месяц', 'месяца', 'месяцев']
+ else:
+ get_list = ['год', 'года', 'лет']
+
+ if get_time % 10 == 1 and get_time % 100 != 11:
+ count = 0
+ elif 2 <= get_time % 10 <= 4 and (get_time % 100 < 10 or get_time % 100 >= 20):
+ count = 1
+ else:
+ count = 2
+
+ return f"{get_time} {get_list[count]}"
+
+
+# Проверка на булевый тип
+def is_bool(value: Union[bool, str, int]) -> bool:
+ value = str(value).lower()
+
+ if value in ('y', 'yes', 't', 'true', 'on', '1'):
+ return True
+ elif value in ('n', 'no', 'f', 'false', 'off', '0'):
+ return False
+ else:
+ raise ValueError(f"invalid truth value {value}")
+
+
+######################################## ЧИСЛА ########################################
+# Преобразование экспоненциальных чисел в читаемый вид (1e-06 -> 0.000001)
+def snum(amount: Union[int, float], remains: int = 2) -> str:
+ format_str = "{:." + str(remains) + "f}"
+ str_amount = format_str.format(float(amount))
+
+ if remains != 0:
+ if "." in str_amount:
+ remains_find = str_amount.find(".")
+ remains_save = remains_find + 8 - (8 - remains) + 1
+
+ str_amount = str_amount[:remains_save]
+
+ if "." in str(str_amount):
+ while str(str_amount).endswith('0'): str_amount = str(str_amount)[:-1]
+
+ if str(str_amount).endswith('.'): str_amount = str(str_amount)[:-1]
+
+ return str(str_amount)
+
+
+# Конвертация любого числа в вещественное, с удалением нулей в конце (remains - округление)
+def to_float(get_number, remains: int = 2) -> Union[int, float]:
+ if "," in str(get_number):
+ get_number = str(get_number).replace(",", ".")
+
+ if "." in str(get_number):
+ get_last = str(get_number).split(".")
+
+ if str(get_last[1]).endswith("0"):
+ while True:
+ if str(get_number).endswith("0"):
+ get_number = str(get_number)[:-1]
+ else:
+ break
+
+ get_number = round(float(get_number), remains)
+
+ str_number = snum(get_number)
+ if "." in str_number:
+ if str_number.split(".")[1] == "0":
+ get_number = int(get_number)
+ else:
+ get_number = float(get_number)
+ else:
+ get_number = int(get_number)
+
+ return get_number
+
+
+# Конвертация вещественного числа в целочисленное
+def to_int(get_number: float) -> int:
+ if "," in get_number:
+ get_number = str(get_number).replace(",", ".")
+
+ get_number = int(round(float(get_number)))
+
+ return get_number
+
+
+# Проверка ввода на число
+def is_number(get_number: Union[str, int, float]) -> bool:
+ if str(get_number).isdigit():
+ return True
+ else:
+ if "," in str(get_number): get_number = str(get_number).replace(",", ".")
+
+ try:
+ float(get_number)
+ return True
+ except ValueError:
+ return False
+
+
+# Преобразование числа в читаемый вид (123456789 -> 123 456 789)
+def format_rate(amount: Union[float, int], around: int = 2) -> str:
+ if "," in str(amount): amount = float(str(amount).replace(",", "."))
+ if " " in str(amount): amount = float(str(amount).replace(" ", ""))
+ amount = str(round(amount, around))
+
+ out_amount, save_remains = [], ""
+
+ if "." in amount: save_remains = amount.split(".")[1]
+ save_amount = [char for char in str(int(float(amount)))]
+
+ if len(save_amount) % 3 != 0:
+ if (len(save_amount) - 1) % 3 == 0:
+ out_amount.extend([save_amount[0]])
+ save_amount.pop(0)
+ elif (len(save_amount) - 2) % 3 == 0:
+ out_amount.extend([save_amount[0], save_amount[1]])
+ save_amount.pop(1)
+ save_amount.pop(0)
+ else:
+ print("Error 4388326")
+
+ for x, char in enumerate(save_amount):
+ if x % 3 == 0: out_amount.append(" ")
+ out_amount.append(char)
+
+ response = "".join(out_amount).strip() + "." + save_remains
+
+ if response.endswith("."):
+ response = response[:-1]
+
+ return response
diff --git a/tgbot/utils/misc/__init__.py b/tgbot/utils/misc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tgbot/utils/misc/bot_commands.py b/tgbot/utils/misc/bot_commands.py
new file mode 100644
index 0000000..4afb310
--- /dev/null
+++ b/tgbot/utils/misc/bot_commands.py
@@ -0,0 +1,49 @@
+# - *- coding: utf- 8 - *-
+from aiogram import Bot
+from aiogram.types import BotCommand, BotCommandScopeChat, BotCommandScopeDefault
+
+from tgbot.data.config import get_admins
+
+# Команды для юзеров
+user_commands = [
+ BotCommand(command="start", description="Restart bot"),
+ BotCommand(command="parser", description="Запускает поиск тендоров"),
+ BotCommand(command="status", description="статус бота"),
+ BotCommand(command="get_notif", description="получать уведомления"),
+ BotCommand(command="stop_get", description="остановить получение уведомлений"),
+ BotCommand(command="get_sheet", description="Получить таблицу"),
+ BotCommand(command="excel_from_tenders", description="Поиск за все время"),
+ BotCommand(command="tenders_with_goods", description="Поиск в автопитере"),
+ BotCommand(command="tenderplan", description="Поиск в tenderplan"),
+ BotCommand(command="log", description="Get Logs"),
+ # BotCommand(command="inline", description="🌀 Get Inline keyboard"),
+]
+
+# Команды для админов
+admin_commands = [
+ BotCommand(command="start", description="Restart bot"),
+ BotCommand(command="parser", description="Запускает поиск тендоров"),
+ BotCommand(command="status", description="статус бота"),
+ BotCommand(command="get_notif", description="получать уведомления"),
+ BotCommand(command="stop_get_notif", description="остановить получение уведомлений"),
+ BotCommand(command="start_shed", description="Запуск работы по расписанию"),
+ BotCommand(command="stop_shed", description="остановка работы по расписанию"),
+ BotCommand(command="get_sheet", description="Получить таблицу"),
+ BotCommand(command="excel_from_tenders", description="Поиск за все время"),
+ BotCommand(command="tenders_with_goods", description="Поиск в автопитере"),
+ BotCommand(command="tenderplan", description="Поиск в tenderplan"),
+ BotCommand(command="log", description="Get Logs"),
+ # BotCommand(command="inline", description="🌀 Get Inline keyboard"),
+ BotCommand(command="db", description="📦 Get Database"),
+]
+
+
+# Установка команд
+async def set_commands(bot: Bot):
+ await bot.set_my_commands(user_commands, scope=BotCommandScopeDefault())
+
+ for admin in get_admins():
+ try:
+ await bot.set_my_commands(admin_commands, scope=BotCommandScopeChat(chat_id=admin))
+ except:
+ pass
diff --git a/tgbot/utils/misc/bot_filters.py b/tgbot/utils/misc/bot_filters.py
new file mode 100644
index 0000000..26b2abc
--- /dev/null
+++ b/tgbot/utils/misc/bot_filters.py
@@ -0,0 +1,22 @@
+# - *- coding: utf- 8 - *-
+from aiogram.filters import BaseFilter
+from aiogram.types import Message
+
+from tgbot.data.config import get_admins
+from tgbot.database.db_users import Userx
+from tgbot.utils.misc.bot_logging import bot_logger
+
+
+# Проверка на админа
+class IsAdmin(BaseFilter):
+ async def __call__(self, message: Message) -> bool:
+ if message.from_user.id in get_admins():
+ return True
+ else:
+ return False
+
+# Отправка сообщения пользователям по тендорам
+def get_employees():
+ get_users = Userx.gets(notif = "True")
+ # bot_logger.warning(f"employee {get_users}")
+ return get_users
\ No newline at end of file
diff --git a/tgbot/utils/misc/bot_logging.py b/tgbot/utils/misc/bot_logging.py
new file mode 100644
index 0000000..a011c36
--- /dev/null
+++ b/tgbot/utils/misc/bot_logging.py
@@ -0,0 +1,32 @@
+# - *- coding: utf- 8 - *-
+import logging as bot_logger
+
+import colorlog
+
+from tgbot.data.config import PATH_LOGS
+
+# Формат логгирования
+log_formatter_file = bot_logger.Formatter("%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s")
+log_formatter_console = colorlog.ColoredFormatter(
+ "%(purple)s%(levelname)s %(blue)s|%(purple)s %(asctime)s %(blue)s|%(purple)s %(filename)s:%(lineno)d %(blue)s|%(purple)s %(message)s%(red)s",
+ datefmt="%d-%m-%Y %H:%M:%S",
+)
+
+# Логгирование в файл logs.log
+file_handler = bot_logger.FileHandler(PATH_LOGS, "w", "utf-8")
+file_handler.setFormatter(log_formatter_file)
+file_handler.setLevel(bot_logger.DEBUG)
+
+# Логгирование в консоль
+console_handler = bot_logger.StreamHandler()
+console_handler.setFormatter(log_formatter_console)
+console_handler.setLevel(bot_logger.DEBUG)
+
+# Подключение настроек логгирования
+bot_logger.basicConfig(
+ format="%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s",
+ handlers=[
+ file_handler,
+ console_handler
+ ]
+)
diff --git a/tgbot/utils/misc/bot_models.py b/tgbot/utils/misc/bot_models.py
new file mode 100644
index 0000000..0e726de
--- /dev/null
+++ b/tgbot/utils/misc/bot_models.py
@@ -0,0 +1,7 @@
+# - *- coding: utf- 8 - *-
+from aiogram.fsm.context import FSMContext
+
+from tgbot.services.api_session import AsyncRequestSession
+
+FSM = FSMContext
+ARS = AsyncRequestSession
diff --git a/tgbot/utils/misc_functions.py b/tgbot/utils/misc_functions.py
new file mode 100644
index 0000000..83f6ba4
--- /dev/null
+++ b/tgbot/utils/misc_functions.py
@@ -0,0 +1,123 @@
+# - *- coding: utf- 8 - *-
+from aiogram import Bot
+from aiogram.types import FSInputFile, BufferedInputFile
+
+from tgbot.data.config import get_admins, PATH_DATABASE, start_status
+from tgbot.utils.const_functions import get_date, send_admins
+from tgbot.utils.misc.bot_logging import bot_logger
+from tgbot.services.parser_tendors import get_tenders_from_url, get_excel_from_tenders
+from tgbot.utils.misc.bot_filters import get_employees
+from tgbot.services.tender_plan import tenders_with_goods, search_in_tenderplan
+import os
+import io
+import pandas as pd
+
+# Отправка сообщения пользователям по тендорам
+# async def get_employees(bot: Bot, text: str, markup=None, not_me=0):
+# get_users = Userx.gets(notif = "False")
+# bot_logger.warning("employee {get_users}")
+
+# Отправка сообщения пользователям по тендорам
+async def send_employees(bot: Bot, text: str, markup=None, not_me=0):
+ get_empl = get_employees()
+ bot_logger.warning(f"employee1 {get_empl}")
+ for empl in get_empl:
+ await bot.send_message(
+ empl.user_id,
+ text,
+ reply_markup=markup,
+ # disable_web_page_preview=True,
+ disable_notification=True,
+ )
+
+ # for admin in get_admins():
+ # try:
+ # if str(admin) != str(not_me):
+ # await bot.send_message(
+ # admin,
+ # text,
+ # reply_markup=markup,
+ # disable_web_page_preview=True,
+ # )
+ # except:
+ # ...
+
+
+# Выполнение функции после запуска бота (рассылка админам о запуске бота)
+async def startup_notify(bot: Bot):
+ if len(get_admins()) >= 1 and start_status:
+ await send_admins(bot, "✅ Bot was started")
+
+
+# Автоматические бэкапы БД
+async def autobackup_admin(bot: Bot):
+ for admin in get_admins():
+ try:
+ await bot.send_document(
+ admin,
+ FSInputFile(PATH_DATABASE),
+ caption=f"📦 #AUTOBACKUP | {get_date()}",
+ )
+ except:
+ pass
+
+
+# поиск тендора по расписанию
+async def tenders_sched(bot: Bot):
+ try:
+ tenders_id = await get_tenders_from_url()
+ bot_logger.warning(f"tenders_id: {tenders_id}")
+ if (len(str(tenders_id))>4000):
+ tenders_id = pd.DataFrame(tenders_id)
+ get_excel_from_tenders(tenders_id)
+ with io.BytesIO() as output:
+ tenders_id.to_excel(output)
+ excel_data = output.getvalue()
+ file_excel = io.BytesIO(excel_data)
+ get_empl = get_employees()
+ bot_logger.warning(f"employee1 {get_empl}")
+ for empl in get_empl:
+ await bot.send_document(
+ empl.user_id,
+ BufferedInputFile(file_excel.getvalue(), f"everyday_articles.xlsx"),
+ caption = f"Нашлось по расписанию",
+ disable_notification=True)
+ else:
+ answ = ""
+ for num, tend in enumerate(tenders_id):
+ answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, прием до: {tend['date_until']}, url: {tend['url_tender']} \n \n"
+ mes = f"Автоматический поиск тендоров: \n \n"
+ if answ == "":
+ mes += "Ничего не найдено"
+ else:
+ mes += answ
+ await send_employees(bot, mes)
+ except:
+ send_admins(bot, f"Ошибка при автоматическом поиске")
+
+
+# поиск тендора в автопитере по расписанию
+async def tenders_sched_ap(bot: Bot):
+ bot_logger.warning(f"tenders_sched_ap start")
+ try:
+ tenders_with_goods(20)
+ await send_employees(bot, "поиск по автопитеру выполнен")
+ except:
+ send_admins(bot, f"Ошибка при поиск по автопитеру")
+
+
+# Поиск тендеров в tenderplan по расписанию
+async def tenders_sched_ap_in_tenderplan(bot: Bot):
+ bot_logger.warning(f"tenders_sched_ap_in_tenderplan")
+ await search_in_tenderplan()
+ get_empl = get_employees()
+ for empl in get_empl:
+ await bot.send_document(
+ empl.user_id,
+ FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'),
+ caption = f"Нашлось из tenderplan по расписанию",
+ disable_notification=True)
+ # await message.answer_document(
+ # FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'),
+ # caption=f"Тендеры из tenderplan.",
+ # )
\ No newline at end of file