From 6af0886a6461ad129e6c4afc5a6b8bc6fa1989e6 Mon Sep 17 00:00:00 2001 From: trevor Date: Fri, 27 Dec 2024 14:47:04 +0900 Subject: [PATCH 1/2] ECVI fully functional --- bot/management/commands/run_bot.py | 67 +------- bot/operations/hotels.py | 37 ++--- bot/operations/statistics.py | 13 +- pms_integration/manager.py | 38 ++--- pms_integration/plugins/base_plugin.py | 20 +-- pms_integration/plugins/ecvi_pms.py | 203 ++++++++++--------------- requierments.txt => requirements.txt | 1 + static/admin/custom.css | 7 + staticfiles/admin/css/custom.css | 7 + touchh/settings.py | 15 ++ 10 files changed, 162 insertions(+), 246 deletions(-) rename requierments.txt => requirements.txt (99%) create mode 100644 static/admin/custom.css create mode 100644 staticfiles/admin/css/custom.css diff --git a/bot/management/commands/run_bot.py b/bot/management/commands/run_bot.py index 76988698..525f113e 100644 --- a/bot/management/commands/run_bot.py +++ b/bot/management/commands/run_bot.py @@ -1,68 +1,3 @@ -# import os -# import django -# import asyncio -# from apscheduler.schedulers.asyncio import AsyncIOScheduler -# from django.core.management.base import BaseCommand -# from telegram.ext import Application -# from bot.utils.bot_setup import setup_bot -# from scheduler.tasks import load_tasks_to_scheduler -# from app_settings.models import TelegramSettings -# from touchh.utils.log import CustomLogger - -# class Command(BaseCommand): -# help = "Запуск Telegram бота и планировщика" - -# def handle(self, *args, **options): -# # Установка Django окружения -# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings") -# django.setup() - -# # Создаем новый цикл событий -# loop = asyncio.new_event_loop() -# asyncio.set_event_loop(loop) - -# # Настройка планировщика -# scheduler = AsyncIOScheduler(event_loop=loop) -# scheduler.start() - -# # Загрузка задач в планировщик -# try: -# load_tasks_to_scheduler(scheduler) -# except Exception as e: -# self.stderr.write(f"Ошибка при загрузке задач в планировщик: {e}") -# return - -# # Настройка Telegram бота -# # bot_token = os.getenv("TELEGRAM_BOT_TOKEN") -# bot_token = TelegramSettings.objects.first().bot_token - -# if not bot_token: -# raise ValueError("Токен бота не найден в переменных окружения.") -# application = Application.builder().token(bot_token).build() -# setup_bot(application) -# # Основная асинхронная функция -# async def main(): -# await application.initialize() -# await application.start() -# await application.updater.start_polling() -# self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены.")) -# try: -# while True: -# await asyncio.sleep(3600) -# except asyncio.CancelledError: -# await application.stop() -# scheduler.shutdown() - -# # Запуск асинхронной программы -# try: -# loop.run_until_complete(main()) -# except KeyboardInterrupt: -# self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика")) -# finally: -# loop.close() - - - import os import django import asyncio @@ -88,7 +23,7 @@ class Command(BaseCommand): bot_token = TelegramSettings.objects.first().bot_token if not bot_token: - raise ValueError("Токен бота не найден в переменных окружения.") + raise ValueError("Токен бота не найден в базе данных.") application = Application.builder().token(bot_token).build() setup_bot(application) diff --git a/bot/operations/hotels.py b/bot/operations/hotels.py index 5598ea09..387b4142 100644 --- a/bot/operations/hotels.py +++ b/bot/operations/hotels.py @@ -124,7 +124,6 @@ async def delete_hotel(update: Update, context): # # Обрабатываем и логируем ошибки # await query.edit_message_text(f"❌ Ошибка: {str(e)}") - async def check_pms(update, context): query = update.callback_query @@ -132,37 +131,38 @@ async def check_pms(update, context): # Получение ID отеля из callback_data hotel_id = query.data.split("_")[2] logger.debug(f"Hotel ID: {hotel_id}") - logger.debug(f"Hotel ID type : {type(hotel_id)}") - - # Получение конфигурации отеля и PMS - hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id) - pms_config = hotel.pms + logger.debug(f"Hotel ID type: {type(hotel_id)}") - if not pms_config: + # Получение объекта отеля с PMS конфигурацией + hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id) + if not hotel.pms: + logger.error(f"Отель {hotel.name} не имеет связанной PMS конфигурации.") await query.edit_message_text("PMS конфигурация не найдена.") return - # Создаем экземпляр PMSIntegrationManager - pms_manager = PMSIntegrationManager(hotel_id=hotel_id) - await pms_manager.load_hotel() + logger.debug(f"Hotel PMS: {hotel.pms.name}") + + # Инициализация PMSIntegrationManager с отелем + pms_manager = PMSIntegrationManager(hotel=hotel) + await sync_to_async(pms_manager.load_hotel)() await sync_to_async(pms_manager.load_plugin)() - # Проверяем, какой способ интеграции использовать + # Проверка наличия fetch_data и вызов плагина if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): - # Плагин поддерживает метод fetch_data - report = await pms_manager.plugin._fetch_data() - logger.debug(f"TYPE: {type(report)}") + report = await pms_manager.plugin.fetch_data() + logger.debug(f"Отчет типа: {type(report)}") else: + logger.error("Плагин не поддерживает fetch_data.") await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") return - # Проверяем результат выполнения fetch_data + # Проверка корректности отчета if not report or not isinstance(report, dict): logger.error(f"Некорректный отчет от fetch_data: {report}") await query.edit_message_text("Ошибка: Отчет fetch_data отсутствует или имеет некорректный формат.") return - # Формируем сообщение о результатах + # Формирование сообщения о результатах result_message = ( f"Интеграция PMS завершена успешно.\n" f"Обработано интервалов: {report.get('processed_intervals', 0)}\n" @@ -174,14 +174,15 @@ async def check_pms(update, context): logger.info(f"Result_Message: {result_message}") await query.edit_message_text(result_message) + except Hotel.DoesNotExist: + logger.error(f"Отель с ID {hotel_id} не найден.") + await query.edit_message_text("Ошибка: Отель не найден.") except Exception as e: # Обрабатываем и логируем ошибки logger.error(f"Ошибка в методе check_pms: {str(e)}", exc_info=True) await query.edit_message_text(f"❌ Ошибка: {str(e)}") - - async def setup_rooms(update: Update, context): """Настроить номера отеля.""" query = update.callback_query diff --git a/bot/operations/statistics.py b/bot/operations/statistics.py index f049c3f9..dc241b95 100644 --- a/bot/operations/statistics.py +++ b/bot/operations/statistics.py @@ -55,10 +55,13 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE context.user_data["selected_hotel"] = hotel_id keyboard = [ - [InlineKeyboardButton("День", callback_data="stats_period_day")], + [InlineKeyboardButton("Сегодня", callback_data="stats_period_today")], + [InlineKeyboardButton("Вчера", callback_data="stats_period_yesterday")], [InlineKeyboardButton("Неделя", callback_data="stats_period_week")], - [InlineKeyboardButton("Месяц", callback_data="stats_period_month")], - [InlineKeyboardButton("Год", callback_data="stats_period_year")], + [InlineKeyboardButton("Этот месяц", callback_data="stats_period_thismonth")], + [InlineKeyboardButton("Прошлый месяц", callback_data="stats_period_lastmonth")], + [InlineKeyboardButton("Этот год", callback_data="stats_period_thisyear")], + [InlineKeyboardButton("Прошлый год", callback_data="stats_period_lastyear")], [InlineKeyboardButton("🏠 Главная", callback_data="main_menu")], [InlineKeyboardButton("🔙 Назад", callback_data="statistics")], ] @@ -140,7 +143,7 @@ def get_period_dates(period, now=None): start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) - elif period == "last_month": + elif period == "lastmonth": # Последний месяц: с первого дня прошлого месяца до последнего дня прошлого месяца first_day_of_current_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) last_day_of_previous_month = first_day_of_current_month - timedelta(days=1) @@ -152,7 +155,7 @@ def get_period_dates(period, now=None): start_date = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) - elif period == "last_year": + elif period == "lastyear": # Последний год: с 1 января предыдущего года до 31 декабря предыдущего года start_date = now.replace(year=now.year - 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0) end_date = now.replace(year=now.year - 1, month=12, day=31, hour=23, minute=59, second=59, microsecond=999999) diff --git a/pms_integration/manager.py b/pms_integration/manager.py index d4052093..8e4065a5 100644 --- a/pms_integration/manager.py +++ b/pms_integration/manager.py @@ -33,37 +33,31 @@ class PluginLoader: return plugins class PMSIntegrationManager: - def __init__(self, hotel_id): - self.hotel_id = hotel_id - self.hotel = None - self.pms_config = None + def __init__(self, hotel): + """ + Инициализирует PMSIntegrationManager с объектом отеля. + :param hotel: Объект отеля, связанный с PMS. + """ + self.hotel = hotel self.plugin = None - async def load_hotel(self): + def load_hotel(self): """ - Загружает данные отеля и PMS конфигурацию. + Проверяет, что у отеля есть связанная PMS конфигурация. """ - from hotels.models import Hotel - self.hotel = await sync_to_async(Hotel.objects.select_related("pms").get)(id=self.hotel_id) - self.pms_config = self.hotel.pms - if not self.pms_config: + if not self.hotel.pms: raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.") def load_plugin(self): """ - Загружает плагин для PMS на основе конфигурации отеля. + Загружает плагин, соответствующий PMS конфигурации отеля. """ - plugins = PluginLoader.load_plugins() - if not self.hotel: - raise ValueError("Отель не загружен. Пожалуйста, вызовите load_hotel перед загрузкой плагина.") - if not self.hotel.pms: - raise ValueError(f"Отель {self.hotel.name} не связан с PMS конфигурацией.") - if self.hotel.pms.plugin_name not in plugins: - raise ValueError(f"Плагин для PMS {self.hotel.pms.plugin_name} не найден.") - - # Передача объекта Hotel в плагин - self.plugin = plugins[self.hotel.pms.plugin_name](self.hotel) - + pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру + if pms_name == "ecvi_intermark" or pms_name == "ecvi": + from pms_integration.plugins.ecvi_pms import EcviPMSPlugin + self.plugin = EcviPMSPlugin(self.hotel) + else: + raise ValueError(f"Неизвестный PMS: {pms_name}") def fetch_data(self): """ Получает данные из PMS с использованием загруженного плагина. diff --git a/pms_integration/plugins/base_plugin.py b/pms_integration/plugins/base_plugin.py index 716a1764..0dc2812d 100644 --- a/pms_integration/plugins/base_plugin.py +++ b/pms_integration/plugins/base_plugin.py @@ -8,26 +8,25 @@ class BasePMSPlugin(ABC): - Предоставлять дефолтные parser_settings - Проходить базовую валидацию (validate_plugin) """ - + def __init__(self, pms_config): """ pms_config: объект PMSConfiguration """ self.pms_config = pms_config - @abstractmethod - def _fetch_data(self): + async def _fetch_data(self): """ Абстрактный метод для получения данных. """ pass - def fetch_data(self): + async def fetch_data(self): """ Обертка для выполнения _fetch_data с возможной дополнительной обработкой. """ - return self._fetch_data() + return await self._fetch_data() @abstractmethod def get_default_parser_settings(self): @@ -45,7 +44,6 @@ class BasePMSPlugin(ABC): } } """ - print("get_default_parser_settings. pms_config:", self.pms_config) return {} def validate_plugin(self): @@ -53,10 +51,8 @@ class BasePMSPlugin(ABC): Проверка на соответствие требованиям. Можно проверить наличие методов или полей. """ - # Например, проверить наличие fetch_data и get_default_parser_settings - required_methods = ["fetch_data", "get_default_parser_settings"] - for m in required_methods: - if not hasattr(self, m): - raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.") - # Можно добавить дополнительные проверки + required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] + for method in required_methods: + if not hasattr(self, method): + raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.") return True diff --git a/pms_integration/plugins/ecvi_pms.py b/pms_integration/plugins/ecvi_pms.py index 73df78df..777c37b5 100644 --- a/pms_integration/plugins/ecvi_pms.py +++ b/pms_integration/plugins/ecvi_pms.py @@ -7,179 +7,136 @@ from .base_plugin import BasePMSPlugin class EcviPMSPlugin(BasePMSPlugin): """ - Плагин для интеграции с PMS Ecvi (интерфейс для получения данных об отеле). + Плагин для интеграции с PMS Ecvi. """ - def __init__(self, pms_config): - super().__init__(pms_config) - - # Инициализация логгера - self.logger = logging.getLogger(self.__class__.__name__) # Логгер с именем класса - handler_console = logging.StreamHandler() # Потоковый обработчик для вывода в консоль - handler_file = logging.FileHandler('ecvi_pms_plugin.log') # Обработчик для записи в файл + def __init__(self, hotel): + super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс + self.hotel = hotel # Сохраняем объект отеля + # Проверка PMS-конфигурации + if not self.hotel.pms: + raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.") + + # Инициализация параметров API + self.api_url = self.hotel.pms.url + self.token = self.hotel.pms.token + self.username = self.hotel.pms.username + self.password = self.hotel.pms.password + + # Настройка логгера + self.logger = logging.getLogger(self.__class__.__name__) + handler_console = logging.StreamHandler() + handler_file = logging.FileHandler('ecvi_pms_plugin.log') formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler_console.setFormatter(formatter) handler_file.setFormatter(formatter) - - # Добавляем оба обработчика к логгеру self.logger.addHandler(handler_console) self.logger.addHandler(handler_file) - self.logger.setLevel(logging.DEBUG) # Уровень логирования - - # Инициализация параметров API - self.api_url = pms_config.url - self.token = pms_config.token - self.username = pms_config.username - self.password = pms_config.password - self.pagination_count = 50 # Максимальное количество записей на страницу (если используется пагинация) + self.logger.setLevel(logging.DEBUG) def get_default_parser_settings(self): """ Возвращает настройки парсера по умолчанию. """ - self.logger.debug(f"get_default_parser_settings. pms_config: {self.pms_config}") return { "field_mapping": { "check_in": "checkin", "check_out": "checkout", - "room_number": "room_name", # Заменили на room_number + "room_number": "room_name", "room_type_name": "room_type", "status": "occupancy", }, - "date_format": "%Y-%m-%dT%H:%M:%S" + "date_format": "%Y-%m-%d %H:%M:%S" # Формат изменен на соответствующий данным } async def _fetch_data(self): """ - Получает данные из PMS API, фильтрует и сохраняет в базу данных. + Получает данные из PMS API и сохраняет их в базу. """ - now = datetime.now() - current_date = now.strftime('%Y-%m-%d') - yesterday_date = (now - timedelta(days=1)).strftime('%Y-%m-%d') - processed_items = 0 - errors = [] - headers = { - "Content-Type": "application/json", - } - data = { - "token": self.token, - } + headers = {"Content-Type": "application/json"} + data = {"token": self.token} try: # Запрос данных из PMS API - response = await sync_to_async(requests.post)(self.api_url, headers=headers, json=data, auth=(self.username, self.password)) - response.raise_for_status() # Если ошибка, выбросит исключение - data = response.json() # Преобразуем ответ в JSON - self.logger.debug(f"Получены данные с API: {data}") + response = await sync_to_async(requests.post)( + self.api_url, headers=headers, json=data, auth=(self.username, self.password) + ) + response.raise_for_status() + response_data = response.json() + self.logger.debug(f"Полученные данные с API: {response_data}") + return await self._process_data(response_data) except requests.exceptions.RequestException as e: - self.logger.error(f"Ошибка запроса: {e}") - return [] + self.logger.error(f"Ошибка API: {e}") + return { + "processed_intervals": 0, + "processed_items": 0, + "errors": [str(e)] + } + + async def _process_data(self, data): + """ + Обрабатывает данные и сохраняет их в базу. + """ + processed_items = 0 + errors = [] + + date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты - # Фильтрация данных - filtered_data = [] for item in data: try: - if not isinstance(item, dict): - raise ValueError(f"Некорректный формат элемента: {item}") + # Парсинг даты с поддержкой нескольких форматов + checkin = self._parse_date(item['checkin'], date_formats) + checkout = self._parse_date(item['checkout'], date_formats) - reservation_id = item.get('task_id') - if not reservation_id: - raise ValueError("Отсутствует task_id в записи") + reservation, created = await sync_to_async(Reservation.objects.update_or_create)( + reservation_id=item['task_id'], + defaults={ + 'room_number': item['room_name'], + 'room_type': item['room_type'], + 'check_in': checkin, + 'check_out': checkout, + 'status': item['occupancy'], + 'hotel': self.hotel, + } + ) - checkin = datetime.strptime(item.get('checkin'), '%Y-%m-%d %H:%M:%S') - checkout = datetime.strptime(item.get('checkout'), '%Y-%m-%d %H:%M:%S') + if created: + self.logger.debug(f"Создана новая резервация: {reservation.reservation_id}") + else: + self.logger.debug(f"Обновлена существующая резервация: {reservation.reservation_id}") - filtered_data.append({ - 'reservation_id': reservation_id, - 'room_number': item.get('room_name'), - 'room_type': item.get('room_type'), - 'checkin': checkin, - 'checkout': checkout, - 'status': item.get('occupancy') - }) processed_items += 1 + except Exception as e: - self.logger.error(f"Ошибка обработки элемента: {e}") + self.logger.error(f"Ошибка обработки записи: {e}") errors.append(str(e)) - # Сохранение данных в базу данных - try: - for item in filtered_data: - await self._save_to_db(item) - except Exception as e: - self.logger.error(f"Ошибка сохранения данных в БД: {e}") - errors.append(f"Ошибка сохранения данных в БД: {str(e)}") - - # Формирование отчета - report = { + return { "processed_intervals": 1, "processed_items": processed_items, "errors": errors } - self.logger.debug(f"Сформированный отчет: {report}") - return report - async def _save_to_db(self, item): + @staticmethod + def _parse_date(date_str, formats): """ - Сохраняет данные в БД (например, информацию о номере). + Парсит дату, пытаясь использовать несколько форматов. """ - try: - # Получаем отель по настройкам PMS - hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) - self.logger.debug(f"Отель найден: {hotel.name}") - - # Проверяем, существует ли уже резервация с таким внешним ID - reservation_id = item.get('reservation_id') - if not reservation_id: - self.logger.error("Ошибка: 'reservation_id' отсутствует в данных.") - return - - existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id) - - # Теперь вызываем .first() после асинхронного вызова - existing_reservation = await sync_to_async(existing_reservation.first)() - - if existing_reservation: - self.logger.debug(f"Резервация {reservation_id} уже существует. Обновляем...") - await sync_to_async(Reservation.objects.update_or_create)( - reservation_id=reservation_id, - defaults={ - '' - 'room_number': item.get('room_number'), - 'room_type': item.get('room_type'), - 'check_in': item.get('checkin'), - 'check_out': item.get('checkout'), - 'status': item.get('status'), - 'hotel': hotel - } - ) - self.logger.debug(f"Резервация обновлена.") - else: - self.logger.debug(f"Резервация не найдена, создаем новую...") - reservation = await sync_to_async(Reservation.objects.create)( - reservation_id=reservation_id, - room_number=item.get('room_number'), - room_type=item.get('room_type'), - check_in=item.get('checkin'), - check_out=item.get('checkout'), - status=item.get('status'), - hotel=hotel - ) - self.logger.debug(f"Новая резервация создана с ID: {reservation.reservation_id}") - - except Exception as e: - self.logger.error(f"Ошибка сохранения данных: {e}") + for fmt in formats: + try: + return datetime.strptime(date_str, fmt) + except ValueError: + continue + raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}") def validate_plugin(self): """ - Проверка на соответствие требованиям. - Можно проверить наличие методов или полей. + Проверка корректности реализации плагина. """ - # Проверяем наличие обязательных методов required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] - for m in required_methods: - if not hasattr(self, m): - raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.") + for method in required_methods: + if not hasattr(self, method): + raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.") self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") return True diff --git a/requierments.txt b/requirements.txt similarity index 99% rename from requierments.txt rename to requirements.txt index 93305621..ea790166 100644 --- a/requierments.txt +++ b/requirements.txt @@ -68,3 +68,4 @@ mysqlclient chardet python-decouple cryptography +mysqlclient \ No newline at end of file diff --git a/static/admin/custom.css b/static/admin/custom.css new file mode 100644 index 00000000..eb16c9ec --- /dev/null +++ b/static/admin/custom.css @@ -0,0 +1,7 @@ +.ml-4 { + margin-left: 1rem !important; +} + +.ml-6 { + margin-left: 1.5rem !important; +} \ No newline at end of file diff --git a/staticfiles/admin/css/custom.css b/staticfiles/admin/css/custom.css new file mode 100644 index 00000000..eb16c9ec --- /dev/null +++ b/staticfiles/admin/css/custom.css @@ -0,0 +1,7 @@ +.ml-4 { + margin-left: 1rem !important; +} + +.ml-6 { + margin-left: 1.5rem !important; +} \ No newline at end of file diff --git a/touchh/settings.py b/touchh/settings.py index 895986e4..adc16aaf 100644 --- a/touchh/settings.py +++ b/touchh/settings.py @@ -188,6 +188,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' JAZZMIN_SETTINGS = { "use_bootstrap5": True, + "custom_css": "admin/custom.css", "site_title": "TOUCHH Hotel Management", "site_header": "TOUCHH Hotel Manager Admin", "site_brand": "TOUCHH", @@ -201,6 +202,20 @@ JAZZMIN_SETTINGS = { "site_icon": None, # Иконка сайта (favicon), например "static/images/favicon.ico" "welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа "copyright": "Touchh", # Кастомный текст в футере + "menu": [ + { + "app": "hotels", + "label": "Управление отелями", + "icon": "fas fa-hotel", + "models": [ + {"model": "hotels.hotel", "label": "Отели"}, + {"model": "hotels.room", "label": "Номера", "css_classes": "ml-6"}, + {"model": "hotels.reservation", "label": "Бронирования", "css_classes": "ml-6"}, + {"model": "hotels.userhotel", "label": "Пользователи отеля", "css_classes": "ml-6"}, + ], + }, + + ], "icons": { # Приложения "hotels": "fas fa-hotel", From ad5c0e699d78c9eecb61a33b70b9664eb836419f Mon Sep 17 00:00:00 2001 From: trevor Date: Fri, 27 Dec 2024 14:47:56 +0900 Subject: [PATCH 2/2] pip freeze --- requirements.txt | 67 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea790166..c00f310b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,4 +68,69 @@ mysqlclient chardet python-decouple cryptography -mysqlclient \ No newline at end of file +mysqlclientace_tools==0.0 +aiohappyeyeballs==2.4.4 +aiohttp==3.11.10 +aiosignal==1.3.1 +anyio==4.6.2.post1 +APScheduler==3.11.0 +asgiref==3.8.1 +async-timeout==5.0.1 +attrs==24.2.0 +certifi==2024.8.30 +cffi==1.17.1 +chardet==5.2.0 +charset-normalizer==3.4.0 +cryptography==44.0.0 +defusedxml==0.7.1 +Django==5.1.4 +django-environ==0.11.2 +django-filter==24.3 +django-health-check==3.18.3 +django-jazzmin==3.0.1 +django-jet==1.0.8 +et_xmlfile==2.0.0 +exceptiongroup==1.2.2 +fonttools==4.55.3 +fpdf2==2.8.2 +frozenlist==1.5.0 +geoip2==4.8.1 +git-filter-repo==2.47.0 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.0 +idna==3.10 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +maxminddb==2.6.2 +multidict==6.1.0 +mysqlclient==2.2.6 +numpy==2.1.3 +openpyxl==3.1.5 +pandas==2.2.3 +pathspec==0.12.1 +pillow==11.0.0 +propcache==0.2.1 +psycopg==3.2.3 +pycparser==2.22 +PyMySQL==1.1.1 +python-dateutil==2.9.0.post0 +python-decouple==3.8 +python-dotenv==1.0.1 +python-telegram-bot==21.8 +pytz==2024.2 +PyYAML==6.0.2 +referencing==0.35.1 +requests==2.32.3 +rpds-py==0.22.3 +six==1.17.0 +sniffio==1.3.1 +sqlparse==0.5.2 +typing_extensions==4.12.2 +tzdata==2024.2 +tzlocal==5.2 +ua-parser==1.0.0 +ua-parser-builtins==0.18.0.post1 +urllib3==2.2.3 +user-agents==2.2.0 +yarl==1.18.3