init main

This commit is contained in:
Zikil 2024-11-03 21:16:44 +07:00
parent 9875b41354
commit 5dced765cb
47 changed files with 3300 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
venv
*pycache*
*.xls*
*.log
*.db

201
LICENSE Normal file
View File

@ -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.

12
dj_bot.conf Normal file
View File

@ -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

83
main.py Normal file
View File

@ -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")

97
requirements.txt Normal file
View File

@ -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

3
settings.ini Normal file
View File

@ -0,0 +1,3 @@
[settings]
bot_token=7096948690:AAGe_b8fOKsLSvjK-Yk703JhEN3ySaqucKo
admin_id=340394898

BIN
tgbot/.DS_Store vendored Normal file

Binary file not shown.

1
tgbot/__init__.py Normal file
View File

@ -0,0 +1 @@

0
tgbot/data/__init__.py Normal file
View File

42
tgbot/data/config.py Normal file
View File

@ -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))

View File

124
tgbot/database/db_helper.py Normal file
View File

@ -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...")

View File

@ -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)

View File

@ -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)

137
tgbot/database/db_users.py Normal file
View File

@ -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)

View File

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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())

View File

@ -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(
"<b>❗ Пожалуйста, не спамьте.\n"
"❗ Please, do not spam.</b>",
)
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(
"<b>❗ Бот не будет отвечать до прекращения спама.\n"
"❗ The bot will not respond until the spam stops.</b>",
)

View File

@ -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)

BIN
tgbot/routers/.DS_Store vendored Normal file

Binary file not shown.

30
tgbot/routers/__init__.py Normal file
View File

@ -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) # Роутер пропущенных апдейтов

View File

View File

@ -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"<b>📦 #BACKUP | <code>{get_date()}</code></b>",
)
# # Получение логов
# @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"<b>🖨 #LOGS | <code>{get_date(full=False)}</code></b>",
# )
# 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("<b>🖨 The logs have been cleared</b>")

View File

@ -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"===================="
)

View File

@ -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}")

235
tgbot/routers/main_start.py Normal file
View File

@ -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"<b>📦 #BACKUP | <code>{get_date()}</code></b>",
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"<b>📦 #BACKUP | <code>{get_date()}</code></b>",
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"<b>🖨 #LOGS | <code>{get_date(full=False)}</code></b>",
)
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.",
)

View File

View File

@ -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)

View File

View File

@ -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()

View File

@ -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&region=&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
# &region=
# &basis=0
# &okved=
# &dateb=&datee=&dateb2=&datee2=
# PL420
# https://www.tender.pro/api/tenders/list?&good_name=PL420&tender_state=100

View File

@ -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'

163
tgbot/services/test.py Normal file
View File

@ -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)

BIN
tgbot/utils/.DS_Store vendored Normal file

Binary file not shown.

0
tgbot/utils/__init__.py Normal file
View File

View File

@ -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 тэгов ('<b>test</b>' -> *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

View File

View File

@ -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

View File

@ -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

View File

@ -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
]
)

View File

@ -0,0 +1,7 @@
# - *- coding: utf- 8 - *-
from aiogram.fsm.context import FSMContext
from tgbot.services.api_session import AsyncRequestSession
FSM = FSMContext
ARS = AsyncRequestSession

View File

@ -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, "<b>✅ Bot was started</b>")
# Автоматические бэкапы БД
async def autobackup_admin(bot: Bot):
for admin in get_admins():
try:
await bot.send_document(
admin,
FSInputFile(PATH_DATABASE),
caption=f"<b>📦 #AUTOBACKUP | <code>{get_date()}</code></b>",
)
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.",
# )