From eaf484625dd413e861c9d967ab0a087ef46a9b84 Mon Sep 17 00:00:00 2001 From: deymonster Date: Fri, 12 Jan 2024 14:12:28 +0500 Subject: [PATCH 01/28] Fix type error in at comparion date --- .github/workflows/main.yml | 0 src/parser/api.py | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/parser/api.py b/src/parser/api.py index cf166b6..b60091f 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -106,6 +106,7 @@ class OperationsByDate: @classmethod def from_dict(cls, data: Dict) -> 'OperationsByDate': """Создание объекта из словаря""" + return cls( date=datetime.fromisoformat(data.get('date', '')), operations=[Operation.from_dict(item) for item in data.get('operations', [])] @@ -366,7 +367,10 @@ def fetch_operations_data(self, date_from: datetime = None, date_to: datetime = all_operations = [OperationsByDate.from_dict(item) for item in details_data] # Фильтрация операций по датам, если они были переданы + if date_from and date_to: + date_from = datetime.strptime(date_from, '%Y-%m-%d') + date_to = datetime.strptime(date_to, '%Y-%m-%d') filtered_operations = [op for op in all_operations if date_from <= op.date <= date_to] return filtered_operations else: From 3f84f163f1b9fa0ca553456d3e99ac9c48f1595c Mon Sep 17 00:00:00 2001 From: deymonster Date: Sun, 28 Jan 2024 14:37:11 +0500 Subject: [PATCH 02/28] Add shortages report, file report in memory --- .github/workflows/main.yml | 50 ++++++++ Makefile | 16 +-- src/main_bot.py | 27 +++- src/parser/api.py | 251 ++++++++++++++++++++++++++++++++----- src/parser/column_names.py | 27 +++- src/utils/env.py | 1 + 6 files changed, 321 insertions(+), 51 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e69de29..046e107 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -0,0 +1,50 @@ +name: Test and Build + +on: + push: + branches: + - main + +jobs: + run_test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + + - name: Install pycodestyle + run: | + python -m pip install --upgrade pip + pip install pycodestyle + + - name: Check codestyle + run: pycodestyle ./src/*.py + + + deploy_to_server: + needs: [run_test] + runs-on: ubuntu-latest + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SERVER_HOST: ${{ secrets.SERVER_HOST }} + SERVER_USERNAME: ${{ secrets.SERVER_USERNAME }} + steps: + - uses: D3rHase/ssh-command-action@v0.2.2 + with: + host: ${{ secrets.SERVER_HOST }} + user: ${{ secrets.SERVER_USERNAME }} + private_key: ${{ secrets.SSH_PRIVATE_KEY }} + command: | + cd wb_parser_bot; + docker rm wb-parser-bot; + make stop; + git pull origin main; + make build; + make run; + + diff --git a/Makefile b/Makefile index 05f5f54..0dc3b62 100644 --- a/Makefile +++ b/Makefile @@ -5,20 +5,20 @@ help: make -pRrq -f $(THIS_FILE) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' build: - docker-compose -f docker-compose.yaml build $(c) + docker compose -f docker-compose.yaml build $(c) run: - docker-compose -f docker-compose.yaml up -d $(c) + docker compose -f docker-compose.yaml up -d $(c) stop: - docker-compose -f docker-compose.yaml stop $(c) + docker compose -f docker-compose.yaml stop $(c) restart: - docker-compose -f docker-compose.yaml stop $(c) - docker-compose -f docker-compose.yaml up -d $(c) + docker compose -f docker-compose.yaml stop $(c) + docker compose -f docker-compose.yaml up -d $(c) destroy: - docker-compose -f docker-compose.yaml down -v $(c) + docker compose -f docker-compose.yaml down -v $(c) log: - docker-compose -f docker-compose.yaml logs --tail=150 -f wb-parser-bot + docker compose -f docker-compose.yaml logs --tail=150 -f wb-parser-bot shell: - docker-compose -f docker-compose.yaml exec wb-parser-bot /bin/bash + docker compose -f docker-compose.yaml exec wb-parser-bot /bin/bash diff --git a/src/main_bot.py b/src/main_bot.py index 6815d12..3a3d552 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -6,7 +6,8 @@ from parser.auth_fr import Auth from bot.logger import WBLogger from parser.api import ParserWB -from parser.column_names import sale_data_column_names_mapping +from parser.column_names import sale_data_column_names_mapping, operations_data_column_names_mapping, \ + shortages_data_column_names_mapping from utils.env import TELEGRAM_TOKEN from telegram import ReplyKeyboardMarkup from bot.utility import restricted @@ -136,6 +137,7 @@ async def send_report(update: Update, context: CallbackContext): parser.fetch_employees() logger.info(" End of fetch employees") date_from_str = context.user_data['start_date'].strftime('%Y-%m-%d') + logger.info(f'Type of date from - {type(date_from_str)}') date_to_str = context.user_data['end_date'].strftime('%Y-%m-%d') report_type = context.user_data.get('report_type') filename = '' @@ -144,12 +146,23 @@ async def send_report(update: Update, context: CallbackContext): data = parser.fetch_sales_data(date_from=date_from_str, date_to=date_to_str) logger.info(f"Get data - {data}") filename = f"sales_data/sales_data_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" - parser.save_to_csv(data=data, filename=filename, column_names_mapings=sale_data_column_names_mapping) + file = parser.save_csv_memory(data=data, filename=filename, column_names_mapping=sale_data_column_names_mapping) + + elif report_type == "shortages": + logger.info("Begin to fetch shortages") + data = parser.fetch_shortages_data(date_from=date_from_str, date_to=date_to_str) + logger.info(f"Get data - {data}") + filename = f"shortages_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" + file = parser.generate_csv_io(data=data, filename=filename) + elif report_type == "operations": logger.info("Begin to fetch operations data") operations_data = parser.fetch_operations_data(date_from=date_from_str, date_to=date_to_str) filename = f"operations_data/operations_data_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" - parser.safe_to_csv_operations(data=operations_data, filename=filename) + file = parser.save_csv_memory(data=operations_data, + filename=filename, + column_names_mapping=operations_data_column_names_mapping) + # parser.safe_to_csv_operations(data=operations_data, filename=filename) elif report_type == "managers": logger.info(f'Begin to fetch managers data') managers_data = parser.fetch_employee_data(date_from=date_from_str, date_to=date_to_str) @@ -171,8 +184,9 @@ async def send_report(update: Update, context: CallbackContext): filename = f"operations_data/manager_operations_data_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" parser.save_to_csv_mananagers_operations(data=managers_data, filename=filename) await update.message.reply_text("Отчет сформирован") - with open(filename, 'rb') as file: - await context.bot.send_document(chat_id=update.effective_chat.id, document=file, filename=filename) + # with open(filename, 'rb') as file: + # await context.bot.send_document(chat_id=update.effective_chat.id, document=file, filename=filename) + await context.bot.send_document(chat_id=update.effective_chat.id, document=file, filename=filename) await update.message.reply_text("Отчет отправлен") return GREET @@ -180,6 +194,7 @@ async def send_report(update: Update, context: CallbackContext): async def show_menu(update: Update) -> int: keyboard = [ ["Отчет по продажам"], + ["Отчет по недостачам"], ["Отчет по операциям"], ["Отчет по менеджерам"], ["Отмена"] @@ -193,6 +208,8 @@ async def choose_report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i user_choice = update.message.text if user_choice == "Отчет по продажам": context.user_data['report_type'] = 'sales' + elif user_choice == "Отчет по недостачам": + context.user_data['report_type'] = 'shortages' elif user_choice == "Отчет по операциям": context.user_data['report_type'] = 'operations' elif user_choice == "Отчет по менеджерам": diff --git a/src/parser/api.py b/src/parser/api.py index b60091f..40b611d 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -1,6 +1,7 @@ import csv -from typing import Dict, Any, Callable, Union -from utils.env import base_url_v2, base_url_v1, refresh_url, operations_url +from typing import Dict, Any, Callable, Union, IO +import io +from utils.env import base_url_v2, base_url_v1, refresh_url, operations_url, shortages_url from datetime import datetime, timedelta import pandas as pd from typing import List, Optional @@ -10,6 +11,74 @@ logger = WBLogger(__name__).get_logger() + +@dataclass +class Shortage: + """Дата класс для хранения недостач""" + shortage_id: int + create_dt: datetime + guilty_employee_id: int + guilty_employee_name: str + amount: float + comment: str + status_id: int + is_history_exist: bool + + @classmethod + def from_dict(cls, data: Dict) -> 'Shortage': + """Создание объекта из словаря""" + return cls( + shortage_id=data['shortage_id'], + create_dt=datetime.fromisoformat(data['create_dt']), + guilty_employee_id=data['guilty_employee_id'], + guilty_employee_name=data['guilty_employee_name'], + amount=data['amount'], + comment=data['comment'], + status_id=data['status_id'], + is_history_exist=data['is_history_exist'] + ) + + +@dataclass +class OfficeShortage: + """ Дата класс для хранения офиса с недостачами""" + office_id: int + office_name: str + office_amount: float + shortages: List[Shortage] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: Dict) -> 'OfficeShortage': + """Создание объекта из словаря""" + shortages_list = data.get("shortages", []) + if shortages_list and isinstance(shortages_list, list): + shortages_list = [Shortage.from_dict(item) for item in shortages_list] + return cls( + office_id=data["office_id"], + office_name=data["office_name"], + office_amount=data["office_amount"], + shortages=shortages_list, + ) + + +@dataclass +class ResponseData: + """ Дата класс для хранения общего списка офисов """ + total_amount: float + offices: List[OfficeShortage] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: Dict) -> 'ResponseData': + """Создание объекта из словаря""" + offices_list = data.get("offices", []) + if offices_list and isinstance(offices_list, list): + offices_list = [cls.from_dict(item) for item in offices_list] + return cls( + total_amount=data["total_amount"], + offices=offices_list, + ) + + @dataclass class Employee: """Класс для хранения данных о сотруднике""" @@ -22,7 +91,7 @@ class Employee: rating: float @classmethod - def from_dict(cls, data: Dict) -> Callable: + def from_dict(cls, data: Dict) -> 'Employee': """Создание объекта из словаря""" if not data.get('is_deleted'): return cls( @@ -53,7 +122,7 @@ def set_barcode(self, barcode: str): self.barcode = barcode @classmethod - def from_dict(cls, data: Dict) -> Callable: + def from_dict(cls, data: Dict) -> 'EmployeeOperations': """Создание объекта класса из словаря""" return cls( date=data['date'], @@ -70,10 +139,10 @@ def from_dict(cls, data: Dict) -> Callable: class Operation: """Класс для хранения операций""" # dt: datetime # поле для хранения даты операции - oper_type: str # тип операции - oper_amount: float # cумма операции - comment: Optional[str] = None # комментарий - тут обычно ШК товара - grouped: List['Operation'] = field(default_factory=list) # дополнительный вложенный список операций + oper_type: str # тип операции + oper_amount: float # cумма операции + comment: Optional[str] = None # комментарий - тут обычно ШК товара + grouped: List['Operation'] = field(default_factory=list) # дополнительный вложенный список операций @classmethod def from_dict(cls, data: Dict) -> 'Operation': @@ -120,6 +189,7 @@ def to_dict(self): # класс для хранения офисов class Office: """Класс для хранения офисов""" + def __init__(self, id: int, name: str, office_shk: str): self.id = id self.name = name @@ -129,7 +199,9 @@ def __init__(self, id: int, name: str, office_shk: str): # класс для хранения данных о продажах class SaleData: """Класс для хранения данных о продажах""" - def __init__(self, office_id: int, + + def __init__(self, + office_id: int, name: str, date: datetime, sale_sum: int, @@ -165,6 +237,7 @@ def to_dict(self): class ParserWB: """Parser for WB API""" + def __init__(self, session): self.base_url_v1 = base_url_v1 self.base_url_v2 = base_url_v2 @@ -184,7 +257,7 @@ def __init__(self, session): } self.session.headers.update(self.headers) - def _get_response_data_wb(self, *, url: str, params: dict, prefix: str): + def _get_response_data_wb(self, *, url: str, params: dict = None, prefix: str): """ Get response data from wb api @@ -199,6 +272,7 @@ def _get_response_data_wb(self, *, url: str, params: dict, prefix: str): "office": f"Ошибка получения данных по офисам {response.status_code}", "sales": f"Ошибка получения данных по продажам {response.status_code}", "reward": f"Ошибка получения данных по вознаграждениям {response.status_code}", + "shortages": f"Ошибка получения данных по недостачам {response.status_code}", "operations": f"Ошибка получения данных по операциям {response.status_code}", "employees": f"Ошибка получения данных по сотрудникам {response.status_code}", "employees_operations": f"Ошибка получения данных по операциям сотрудников {response.status_code}", @@ -237,7 +311,7 @@ def fetch_employee_data(self, date_from: datetime, date_to: datetime) -> List[Di :param date_from: date from :param date_to: date to - :return: dict with employee operations + :return: List of dict with employee operations """ url = f"{self.base_url_v2}/employees/proceeds" logger.info(f'Url - {url}') @@ -259,7 +333,8 @@ def fetch_employee_data(self, date_from: datetime, date_to: datetime) -> List[Di operation = EmployeeOperations.from_dict(data) if operation: operations_employee.append(operation) - employee_info = next((e for e in self.employees if e.employee_id == employee_data['employee_id']), None) + employee_info = next((e for e in self.employees if e.employee_id == employee_data['employee_id']), + None) all_operations.append({ 'employee_id': employee_data['employee_id'], 'last_name': employee_info.last_name if employee_info else None, @@ -275,6 +350,35 @@ def fetch_employee_data(self, date_from: datetime, date_to: datetime) -> List[Di logger.error(e) return all_operations + def fetch_shortages_data(self, date_from: str = None, date_to: str = None): + """Fetch all shortages from all offices """ + result = [] + date_from = datetime.strptime(date_from, '%Y-%m-%d') + date_to = datetime.strptime(date_to, '%Y-%m-%d') + if shortages_response := self._get_response_data_wb(url=shortages_url, prefix='shortages'): + + for data in (shortages_response.get('offices') or []): + # Преобразование JSON в объект ResponseData + shortages_office_data = OfficeShortage.from_dict(data) + # Применение фильтрации по датам, если они были переданы + if date_from and date_to: + filtered_shortages = [ + shortage for shortage in shortages_office_data.shortages + if date_from <= shortage.create_dt.replace(tzinfo=None) <= date_to + ] + if filtered_shortages: + result.append( + OfficeShortage( + office_id=shortages_office_data.office_id, + office_name=shortages_office_data.office_name, + office_amount=shortages_office_data.office_amount, + shortages=filtered_shortages + ) + ) + else: + result.append(shortages_office_data) + return result + def fetch_offices(self): """Get offices from wb api - Получение всех офисов из API WB""" url = f"{self.base_url_v1}/account" @@ -291,7 +395,7 @@ def fetch_offices(self): except Exception as e: logger.error(e) - def fetch_sales_data(self, date_from: datetime = None, date_to: datetime = None) -> Union[ + def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ list[Any], list[SaleData]]: """Get sales data from wb api - Получение данных по продажам """ result = [] @@ -322,7 +426,6 @@ def fetch_sales_data(self, date_from: datetime = None, date_to: datetime = None) # запрос данных по вознаграждениям rewards_data_dict = {} if reward_response := self._get_response_data_wb(url=url_reward, params=params, prefix='reward'): - rewards_data_dict = {reward['date']: reward for reward in reward_response} for date, sale in sales_data_dict.items(): @@ -335,9 +438,9 @@ def fetch_sales_data(self, date_from: datetime = None, date_to: datetime = None) supplier_return_sum = reward_data['ext_data']['supplier_return_sum'] if reward_data else 0 sale_object = SaleData( - date=datetime.strptime(date, '%Y-%m-%d'), office_id=office_id, name=sales_data['office_name'], + date=datetime.strptime(date, '%Y-%m-%d'), sale_count=sale['sale_count'], return_count=sale['return_count'], sale_sum=sale['sale_sum'], @@ -351,6 +454,7 @@ def fetch_sales_data(self, date_from: datetime = None, date_to: datetime = None) supplier_return_sum=supplier_return_sum, ) + logger.info(f'Обработана дата {date} для офиса {office_id} -- {sale_object.name}') result.append(sale_object) @@ -376,6 +480,98 @@ def fetch_operations_data(self, date_from: datetime = None, date_to: datetime = else: return all_operations + def generate_csv_io(self, data: List[OfficeShortage], filename: str): + # Объект в памяти для записи CSV + csv_buffer = io.StringIO() + + # Создаем объект writer для записи в CSV + csv_writer = csv.DictWriter(csv_buffer, + fieldnames=["Office ID", "Название офиса", "Общая недостача офиса", "ID недостачи", + "Дата недостачи", "ID сотрудника", "Ф.И.О сотрудника", + "Сумма недостачи", "Причина недостачи", "Status ID", + "is history exists"]) + csv_writer.writeheader() + for office in data: + for shortage in office.shortages: + + csv_writer.writerow({ + "Office ID": office.office_id, + "Название офиса": office.office_name, + "Общая недостача офиса": office.office_amount, + "ID недостачи": shortage.shortage_id, + "Дата недостачи": shortage.create_dt, + "ID сотрудника": shortage.guilty_employee_id, + "Ф.И.О сотрудника": shortage.guilty_employee_name, + "Сумма недостачи": shortage.amount, + "Причина недостачи": shortage.comment, + "Status ID": shortage.status_id, + "is history exists": shortage.is_history_exist + }) + csv_buffer.seek(0) + buf = io.BytesIO() + # extract csv-string, convert it to bytes and write to buffer + buf.write(csv_buffer.getvalue().encode()) + buf.seek(0) + # set a filename with file's extension + buf.name = filename + bytes_data = buf.getvalue() + + return bytes_data + + + def save_csv_memory(self, + data: List[object], + filename: str, + column_names_mapping: Dict[str, str] + ): + def process_operations(operations, date): + for operation in operations: + if isinstance(operation, Operation): + op_dict = operation.to_dict() + else: + op_dict = operation + op_dict['date'] = date + filtered_item_dict = {csv_key: op_dict[obj_key] for obj_key, csv_key in column_names_mapping.items() if + obj_key in op_dict} + + csv_writer.writerow(filtered_item_dict) + logger.info(f'Записаны данные в строку файла - {filtered_item_dict}') + # Рекурсивно обрабатываем вложенные операции, если они есть + if 'grouped' in op_dict and op_dict['grouped']: + process_operations(op_dict['grouped'], date) + + # Получаем имена столбцов из словаря column_names_mapping + column_names = list(column_names_mapping.values()) + # Объект в памяти для записи CSV + csv_buffer = io.StringIO() + # Создаем объект writer для записи в CSV + csv_writer = csv.DictWriter(csv_buffer, + fieldnames=column_names) + csv_writer.writeheader() + for item in data: + item_dict = item.__dict__ + # обработка операций + if 'operations' in item_dict and item_dict['operations']: + process_operations(item_dict['operations'], item_dict['date']) + + else: + + filtered_item_dict = {csv_key: item_dict[obj_key] for obj_key, csv_key in column_names_mapping.items() + if obj_key in item_dict} + csv_writer.writerow(filtered_item_dict) + logger.info(f'Записаны данные в строку файла - {filtered_item_dict}') + + csv_buffer.seek(0) + buf = io.BytesIO() + # extract csv-string, convert it to bytes and write to buffer + buf.write(csv_buffer.getvalue().encode()) + buf.seek(0) + + # Устанавливаем имя файла с расширением + buf.name = filename + bytes_data = buf.getvalue() + return bytes_data + def save_to_csv(self, data: List[object], filename: str, @@ -388,7 +584,8 @@ def save_to_csv(self, df = df.explode('operations') # Разбиваем операции на отдельные колонки operations_df = pd.DataFrame(df['operations'].to_list()) - df = pd.concat([df.drop(['operations'], axis=1).reset_index(drop=True), operations_df.reset_index(drop=True)], axis=1) + df = pd.concat( + [df.drop(['operations'], axis=1).reset_index(drop=True), operations_df.reset_index(drop=True)], axis=1) if oper_type_mapings: df['oper_type'] = df['oper_type'].map(oper_type_mapings) @@ -396,7 +593,8 @@ def save_to_csv(self, if 'grouped' in df.columns: df = df.explode('grouped') grouped_df = pd.DataFrame(df['grouped'].dropna().to_list()) - df = pd.concat([df.drop(['grouped'], axis=1).reset_index(drop=True), grouped_df.reset_index(drop=True)], axis=1) + df = pd.concat([df.drop(['grouped'], axis=1).reset_index(drop=True), grouped_df.reset_index(drop=True)], + axis=1) df.rename(columns=column_names_mapings, inplace=True) df.to_csv(filename, index=False) return None @@ -434,7 +632,8 @@ def safe_to_csv_operations(self, def save_to_csv_mananagers_operations(self, data: List[Dict], filename: str): with open(filename, 'w', newline='') as csvfile: fieldnames = ['ID сотрудника', 'Фамилия', 'Имя', 'Отчество', 'Телефон', 'Дата трудоустройства', 'Рейтинг', - 'Дата операции', 'Принято вещей', 'Возвраты', 'Возвраты (сумма)', 'Продажи', 'Продажи (сумма)', + 'Дата операции', 'Принято вещей', 'Возвраты', 'Возвраты (сумма)', 'Продажи', + 'Продажи (сумма)', 'ШК офиса'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() @@ -459,19 +658,3 @@ def save_to_csv_mananagers_operations(self, data: List[Dict], filename: str): }) else: logger.info(f'Нет данных по операциям для сотрудника {employee["employee_id"]}') - - - - - - - - - - - - - - - - diff --git a/src/parser/column_names.py b/src/parser/column_names.py index 4f4bf64..04f6144 100644 --- a/src/parser/column_names.py +++ b/src/parser/column_names.py @@ -3,25 +3,44 @@ 'office_id': 'ID офиса', 'name': 'Название офиса', 'date': 'Дата', - 'sale_count': 'Продажи', - 'return_count': 'Возвраты', + 'sale_count': 'Кол-во продаж', + 'return_count': 'Кол-во возвратов', 'sale_sum': 'Продажи РУБ', 'return_sum': 'Возвраты РУБ', 'proceeds': 'Объем продаж РУБ', 'amount': 'Вознаграждение РУБ', 'bags_sum': 'Пакеты РУБ', 'office_rating': 'Рейтинг ПВЗ', - 'percent': 'Тариф (ставка грейда)' + 'percent': 'Тариф (ставка грейда)', + 'office_rating_sum': 'Сумма рейтинга', + 'supplier_return_sum': 'Сумма возвратов поставщика' } + operations_data_column_names_mapping = { 'date': 'Дата', 'oper_type': 'Тип операции', 'oper_amount': 'Сумма операции', - 'oper_comment': 'ШК', + 'comment': 'ШК', +} + +shortages_data_column_names_mapping = { + "office_id": "Office ID", + "office_name": "Название офиса", + "office_amount": "Общая недостача офиса", + "shortage_id": "ID недостачи", + "create_dt": "Дата недостачи", + "guilty_employee_id": "ID сотрудника", + "guilty_employee_name": "Ф.И.О сотрудника", + "amount": "Сумма недостачи", + "comment": "Причина недостачи", + "status_id": "Status ID", + "is_history_exist": "is history exists - история?" } + + oper_type_mapping= { 1: 'Недостача', 2: 'Премирование', diff --git a/src/utils/env.py b/src/utils/env.py index e155b04..6362ab2 100644 --- a/src/utils/env.py +++ b/src/utils/env.py @@ -12,6 +12,7 @@ refresh_token = os.getenv('refresh_token') basic = os.getenv('BASIC') operations_url = os.getenv('OPERATIONS_URL') +shortages_url = os.getenv('SHORTAGES_URL') date_from = os.getenv('DATE_FROM') date_to = os.getenv('DATE_TO') GET_EVENTS_LK_URL = os.getenv('GET_EVENTS_LK_URL') From 9d6a606a324d7bad92298845112fe77a726c7950 Mon Sep 17 00:00:00 2001 From: deymonster Date: Sun, 28 Jan 2024 14:51:10 +0500 Subject: [PATCH 03/28] Fix codestyle a little --- src/main_bot.py | 69 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/src/main_bot.py b/src/main_bot.py index 3a3d552..5c34de5 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -1,13 +1,15 @@ from telegram import Update -from telegram.ext import (CommandHandler, ContextTypes, ConversationHandler, - CallbackContext, ApplicationBuilder, MessageHandler, filters) +from telegram.ext import (CommandHandler, ContextTypes, + ConversationHandler, + CallbackContext, ApplicationBuilder, + MessageHandler, filters) from bot.utility import validate_phone_number, validate_date from parser.auth_api import AuthApi from parser.auth_fr import Auth from bot.logger import WBLogger from parser.api import ParserWB -from parser.column_names import sale_data_column_names_mapping, operations_data_column_names_mapping, \ - shortages_data_column_names_mapping +from parser.column_names import sale_data_column_names_mapping, \ + operations_data_column_names_mapping from utils.env import TELEGRAM_TOKEN from telegram import ReplyKeyboardMarkup from bot.utility import restricted @@ -32,32 +34,42 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: :return: int""" logger.info("Start begin") user = update.message.from_user - await update.message.reply_text(f"Приветствую тебя {user.first_name} для продолжения необходимо авторизоваться в ЛК. Введите номер телефона привязанный к ЛК WB") + await update.message.reply_text( + f"Приветствую тебя {user.first_name} " + f"для продолжения необходимо авторизоваться в ЛК. " + f"Введите номер телефона привязанный к ЛК WB") return GET_PHONE -async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - """ Состояние для получения номера телефона для авторизации, проверка наличия токена, - если токен валидный то получаем сессию для работы, если токена нет или просрочен то +async def get_phone(update: Update, + context: ContextTypes.DEFAULT_TYPE) -> int: + """ Состояние для получения номера телефона + для авторизации, проверка наличия токена, + если токен валидный то получаем сессию для работы, + если токена нет или просрочен то ожидаем от пользователя код из ЛК """ phone_number = update.message.text phone = validate_phone_number(phone_number) if phone_number == "Отмена": - await update.message.reply_text("Выберите действие или начните заново командой /start") + await update.message.reply_text( + "Выберите действие или начните заново командой /start") return GREET if phone is None: - await update.message.reply_text("Неверный номер телефона. Попробуйте еще раз") + await update.message.reply_text( + "Неверный номер телефона. Попробуйте еще раз") return GET_PHONE context.user_data['auth'] = Auth() context.user_data["phone"] = phone session = context.user_data['auth'].get_franchise_session(phone) auth_status = context.user_data['auth'].get_auth_status() if auth_status == "NEED_CODE": - await update.message.reply_text(f"Отлично! Вы ввели номер {phone}, теперь введите код из ЛК") + await update.message.reply_text( + f"Отлично! Вы ввели номер {phone}, теперь введите код из ЛК") return GET_CODE elif auth_status == "ERROR": - await update.message.reply_text("Слишком много запросов кода. Повторите запрос позднее") + await update.message.reply_text( + "Слишком много запросов кода. Повторите запрос позднее") return ConversationHandler.END else: await update.message.reply_text(f"Вы уже авторизованы с номером {phone}!") @@ -66,11 +78,13 @@ async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def get_code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - """ Состояние для получения кода из ЛК от пользователя, далее отправка телефона и кода для получения + """ Состояние для получения кода из ЛК от пользователя, + далее отправка телефона и кода для получения новой сессии""" code = update.message.text if code == "Отмена": - await update.message.reply_text("Выберите действие или начните заново командой /start") + await update.message.reply_text( + "Выберите действие или начните заново командой /start") return GREET context.user_data['code'] = code phone = context.user_data['phone'] @@ -92,15 +106,18 @@ async def get_code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: # return GET_START_DATE -async def get_start_date(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: +async def get_start_date(update: Update, + context: ContextTypes.DEFAULT_TYPE) -> int: """ Состояние для получения от пользователя даты начала отчета""" start_date = update.message.text if start_date == "Отмена": - await update.message.reply_text("Выберите действие или начните заново командой /start") + await update.message.reply_text( + "Выберите действие или начните заново командой /start") return GREET start_date = validate_date(start_date) if start_date is None: - await update.message.reply_text("Неверный формат даты. Попробуйте еще раз") + await update.message.reply_text( + "Неверный формат даты. Попробуйте еще раз") return GET_START_DATE context.user_data['start_date'] = start_date await update.message.reply_text(f"Отлично! Дата начала отчета: {start_date}. " @@ -157,15 +174,18 @@ async def send_report(update: Update, context: CallbackContext): elif report_type == "operations": logger.info("Begin to fetch operations data") - operations_data = parser.fetch_operations_data(date_from=date_from_str, date_to=date_to_str) - filename = f"operations_data/operations_data_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" + operations_data = parser.fetch_operations_data(date_from=date_from_str, + date_to=date_to_str) + filename = f"operations_data/operations_data_{date_from_str} " \ + f"- {date_to_str} - {context.user_data['phone']}.csv" file = parser.save_csv_memory(data=operations_data, filename=filename, column_names_mapping=operations_data_column_names_mapping) # parser.safe_to_csv_operations(data=operations_data, filename=filename) elif report_type == "managers": logger.info(f'Begin to fetch managers data') - managers_data = parser.fetch_employee_data(date_from=date_from_str, date_to=date_to_str) + managers_data = parser.fetch_employee_data(date_from=date_from_str, + date_to=date_to_str) api_parser = AuthApi() api_parser.get_token() for employee in managers_data: @@ -175,13 +195,16 @@ async def send_report(update: Update, context: CallbackContext): logger.info(f'API data - {api_data}') if api_data: for day in api_data: - api_date = day.get('date_created').split('T')[0] # получаем дату записи входа - api_barcode = day.get('barcode') # получаем ШК офиса + # дата записи входа + api_date = day.get('date_created').split('T')[0] + # получаем ШК офиса + api_barcode = day.get('barcode') for operation in employee.get('operations'): operation_date = operation.date if api_date == operation_date: operation.barcode = api_barcode - filename = f"operations_data/manager_operations_data_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" + filename = f"operations_data/manager_operations_data_{date_from_str} " \ + f"- {date_to_str} - {context.user_data['phone']}.csv" parser.save_to_csv_mananagers_operations(data=managers_data, filename=filename) await update.message.reply_text("Отчет сформирован") # with open(filename, 'rb') as file: From 76682ba56e6ec75cdc8bafe4a40f0b095b0e97cc Mon Sep 17 00:00:00 2001 From: deymonster Date: Sun, 28 Jan 2024 14:58:25 +0500 Subject: [PATCH 04/28] Remove some files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index dba0526..d04bac9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ __pycache__/ *.DS_Store .idea/ + +.src/main.py +.src/test_parser.py \ No newline at end of file From e4bd406df51d2d41d38bdc27572107cb5404bafe Mon Sep 17 00:00:00 2001 From: deymonster Date: Sun, 28 Jan 2024 15:09:37 +0500 Subject: [PATCH 05/28] Remove test --- .github/workflows/main.yml | 38 +++++++++++++++++++------------------- src/main_bot.py | 7 ++----- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 046e107..934fcc3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,28 +6,28 @@ on: - main jobs: - run_test: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.11 - - - name: Install pycodestyle - run: | - python -m pip install --upgrade pip - pip install pycodestyle - - - name: Check codestyle - run: pycodestyle ./src/*.py +# run_test: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v2 +# +# - name: Set up Python +# uses: actions/setup-python@v2 +# with: +# python-version: 3.11 +# +# - name: Install pycodestyle +# run: | +# python -m pip install --upgrade pip +# pip install pycodestyle +# +# - name: Check codestyle +# run: pycodestyle ./src/*.py deploy_to_server: - needs: [run_test] +# needs: [run_test] runs-on: ubuntu-latest env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} diff --git a/src/main_bot.py b/src/main_bot.py index 5c34de5..7aacd4d 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -9,21 +9,20 @@ from bot.logger import WBLogger from parser.api import ParserWB from parser.column_names import sale_data_column_names_mapping, \ - operations_data_column_names_mapping + operations_data_column_names_mapping from utils.env import TELEGRAM_TOKEN from telegram import ReplyKeyboardMarkup from bot.utility import restricted import sys import os - sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) # set up logging logger = WBLogger(__name__).get_logger() GREET, GET_PHONE, GET_CODE, GET_START_DATE, GET_END_DATE = range(5) -SALES_REPORT, OPERATIONS_REPORT = range(6,8) +SALES_REPORT, OPERATIONS_REPORT = range(6, 8) @restricted @@ -273,5 +272,3 @@ def main() -> None: if __name__ == "__main__": main() - - From b72d395b263af8d4001d6782f282f70cf6165524 Mon Sep 17 00:00:00 2001 From: deymonster Date: Tue, 30 Jan 2024 11:40:36 +0500 Subject: [PATCH 06/28] Add all info about each shortage --- src/parser/api.py | 165 +++++++++++++++++++++++++++++++++++----------- src/utils/env.py | 1 + 2 files changed, 129 insertions(+), 37 deletions(-) diff --git a/src/parser/api.py b/src/parser/api.py index 40b611d..5c5de52 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -1,7 +1,7 @@ import csv from typing import Dict, Any, Callable, Union, IO import io -from utils.env import base_url_v2, base_url_v1, refresh_url, operations_url, shortages_url +from utils.env import base_url_v2, base_url_v1, refresh_url, operations_url, shortages_url, shks_url from datetime import datetime, timedelta import pandas as pd from typing import List, Optional @@ -12,6 +12,66 @@ logger = WBLogger(__name__).get_logger() +@dataclass +class FoundInfo: + """Датакласс для foundinfo""" + found_in_office_id: int + found_at: datetime + found_by_employee_id: int + operation: str + + @classmethod + def from_dict(cls, data: Dict) -> 'FoundInfo': + """Создание объекта FoundInfo из словаря""" + return cls( + found_in_office_id=data['found_in_office_id'], + found_at=datetime.fromisoformat(data['found_at']), + found_by_employee_id=data['found_by_employee_id'], + operation=data['operation'] + ) + + +@dataclass +class Shk: + """Датакласс для хранения ШК""" + amount: int + found_info: Optional[FoundInfo] + item_name: str + item_photo_url: str + item_site_url: str + new_shk_id: int + shk_id: int + + @classmethod + def from_dict(cls, data: Dict) ->'Shk': + """Создание объекта Shk из словаря""" + return cls( + amount=data['amount'], + found_info=FoundInfo.from_dict(data['found_info']) if data.get('found_info') else None, + item_name=data['item_name'], + item_photo_url=data['item_photo_url'], + item_site_url=data['item_site_url'], + new_shk_id=data['new_shk_id'] if data.get('new_shk_id') else None, + shk_id=data['shk_id'] + ) + + +@dataclass +class ResponseShk: + """Дата класс для ответа по shortage_id""" + shks: List[Shk] = field(default_factory=list) + + @staticmethod + def from_dict(cls, data: Dict): + """Создание объекта ResponseShk из словаря""" + shks_list = data.get("shks", []) + if shks_list and isinstance(shks_list, list): + shks_list = [Shk.from_dict(item) for item in shks_list] + return cls( + shks=shks_list + ) + + @dataclass class Shortage: """Дата класс для хранения недостач""" @@ -23,10 +83,12 @@ class Shortage: comment: str status_id: int is_history_exist: bool + shks_data: Optional[ResponseShk] = None @classmethod def from_dict(cls, data: Dict) -> 'Shortage': """Создание объекта из словаря""" + shks_data = data.get('shks_data') return cls( shortage_id=data['shortage_id'], create_dt=datetime.fromisoformat(data['create_dt']), @@ -35,7 +97,8 @@ def from_dict(cls, data: Dict) -> 'Shortage': amount=data['amount'], comment=data['comment'], status_id=data['status_id'], - is_history_exist=data['is_history_exist'] + is_history_exist=data['is_history_exist'], + shks_data=ResponseShk.from_dict(shks_data) if shks_data else None ) @@ -61,23 +124,6 @@ def from_dict(cls, data: Dict) -> 'OfficeShortage': ) -@dataclass -class ResponseData: - """ Дата класс для хранения общего списка офисов """ - total_amount: float - offices: List[OfficeShortage] = field(default_factory=list) - - @classmethod - def from_dict(cls, data: Dict) -> 'ResponseData': - """Создание объекта из словаря""" - offices_list = data.get("offices", []) - if offices_list and isinstance(offices_list, list): - offices_list = [cls.from_dict(item) for item in offices_list] - return cls( - total_amount=data["total_amount"], - offices=offices_list, - ) - @dataclass class Employee: @@ -273,6 +319,7 @@ def _get_response_data_wb(self, *, url: str, params: dict = None, prefix: str): "sales": f"Ошибка получения данных по продажам {response.status_code}", "reward": f"Ошибка получения данных по вознаграждениям {response.status_code}", "shortages": f"Ошибка получения данных по недостачам {response.status_code}", + "shks": f"Ошибка получения данных по ШК в недостаче {response.status_code}", "operations": f"Ошибка получения данных по операциям {response.status_code}", "employees": f"Ошибка получения данных по сотрудникам {response.status_code}", "employees_operations": f"Ошибка получения данных по операциям сотрудников {response.status_code}", @@ -358,7 +405,7 @@ def fetch_shortages_data(self, date_from: str = None, date_to: str = None): if shortages_response := self._get_response_data_wb(url=shortages_url, prefix='shortages'): for data in (shortages_response.get('offices') or []): - # Преобразование JSON в объект ResponseData + # Преобразование JSON в объект OfficeShortage shortages_office_data = OfficeShortage.from_dict(data) # Применение фильтрации по датам, если они были переданы if date_from and date_to: @@ -366,6 +413,18 @@ def fetch_shortages_data(self, date_from: str = None, date_to: str = None): shortage for shortage in shortages_office_data.shortages if date_from <= shortage.create_dt.replace(tzinfo=None) <= date_to ] + for shortage in filtered_shortages: + if shks_response := self._get_response_data_wb(url=shks_url, + params={'shortage_id': shortage.shortage_id}, + prefix='shks'): + shks_list = shks_response.get('shks', []) + shks_objects = [Shk.from_dict(item) for item in shks_list] + shks_data = ResponseShk( + shks=shks_objects + ) + + shortage.shks_data = shks_data + if filtered_shortages: result.append( OfficeShortage( @@ -486,27 +545,59 @@ def generate_csv_io(self, data: List[OfficeShortage], filename: str): # Создаем объект writer для записи в CSV csv_writer = csv.DictWriter(csv_buffer, - fieldnames=["Office ID", "Название офиса", "Общая недостача офиса", "ID недостачи", - "Дата недостачи", "ID сотрудника", "Ф.И.О сотрудника", - "Сумма недостачи", "Причина недостачи", "Status ID", - "is history exists"]) + fieldnames=[ + "Office ID", + "Название офиса", + "Общая недостача офиса", + "ID недостачи", + "Дата недостачи", + "ID сотрудника", + "Ф.И.О сотрудника", + "Сумма недостачи", + "Причина недостачи", + "Status ID", + "is history exists", + "Стоимость недостачи по ШК", + "Наименование товара", + "URL на фото товара", + "URL товара в каталоге", + "Новый ШК", + "Основной ШК", + "Найдено в офисе ID", + "Дата находки", + "Кем найдено ID сотрудника", + "Наименование операции" + + ]) csv_writer.writeheader() for office in data: for shortage in office.shortages: - - csv_writer.writerow({ - "Office ID": office.office_id, - "Название офиса": office.office_name, - "Общая недостача офиса": office.office_amount, - "ID недостачи": shortage.shortage_id, - "Дата недостачи": shortage.create_dt, - "ID сотрудника": shortage.guilty_employee_id, - "Ф.И.О сотрудника": shortage.guilty_employee_name, - "Сумма недостачи": shortage.amount, - "Причина недостачи": shortage.comment, - "Status ID": shortage.status_id, - "is history exists": shortage.is_history_exist + for shks_data in shortage.shks_data.shks: + found_info = shks_data.found_info + csv_writer.writerow({ + "Office ID": office.office_id, + "Название офиса": office.office_name, + "Общая недостача офиса": office.office_amount, + "ID недостачи": shortage.shortage_id, + "Дата недостачи": shortage.create_dt, + "ID сотрудника": shortage.guilty_employee_id, + "Ф.И.О сотрудника": shortage.guilty_employee_name, + "Сумма недостачи": shortage.amount, + "Причина недостачи": shortage.comment, + "Status ID": shortage.status_id, + "is history exists": shortage.is_history_exist, + "Стоимость недостачи по ШК": shks_data.amount, + "Наименование товара": shks_data.item_name, + "URL на фото товара": shks_data.item_photo_url, + "URL товара в каталоге": shks_data.item_site_url, + "Новый ШК": shks_data.new_shk_id, + "Основной ШК": shks_data.shk_id, + "Найдено в офисе ID": found_info.found_in_office_id if found_info else None, + "Дата находки": found_info.found_at if found_info else None, + "Кем найдено ID сотрудника": found_info.found_by_employee_id if found_info else None, + "Наименование операции": found_info.operation if found_info else None }) + csv_buffer.seek(0) buf = io.BytesIO() # extract csv-string, convert it to bytes and write to buffer diff --git a/src/utils/env.py b/src/utils/env.py index 6362ab2..4fe9ff4 100644 --- a/src/utils/env.py +++ b/src/utils/env.py @@ -13,6 +13,7 @@ basic = os.getenv('BASIC') operations_url = os.getenv('OPERATIONS_URL') shortages_url = os.getenv('SHORTAGES_URL') +shks_url = os.getenv("SHKS_URL") date_from = os.getenv('DATE_FROM') date_to = os.getenv('DATE_TO') GET_EVENTS_LK_URL = os.getenv('GET_EVENTS_LK_URL') From 94ccce47835f947fd40d23b59909b9abcc83f186 Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 15 Feb 2024 14:48:25 +0500 Subject: [PATCH 07/28] Add pass Office with None data --- src/parser/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/api.py b/src/parser/api.py index 5c5de52..73dbc8d 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -43,7 +43,7 @@ class Shk: shk_id: int @classmethod - def from_dict(cls, data: Dict) ->'Shk': + def from_dict(cls, data: Dict) -> 'Shk': """Создание объекта Shk из словаря""" return cls( amount=data['amount'], @@ -124,7 +124,6 @@ def from_dict(cls, data: Dict) -> 'OfficeShortage': ) - @dataclass class Employee: """Класс для хранения данных о сотруднике""" @@ -477,6 +476,8 @@ def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ if sale_response := self._get_response_data_wb(url=url_sales, params=params, prefix='sales'): try: sales_data = sale_response[0] + if sales_data['by_office'] is None: + continue sales_data_dict = {sale['date']: sale for sale in sales_data['by_office']} except Exception as e: logger.error(f'Ошибка при обработке данных для офиса {office_id}: {e}') @@ -596,7 +597,7 @@ def generate_csv_io(self, data: List[OfficeShortage], filename: str): "Дата находки": found_info.found_at if found_info else None, "Кем найдено ID сотрудника": found_info.found_by_employee_id if found_info else None, "Наименование операции": found_info.operation if found_info else None - }) + }) csv_buffer.seek(0) buf = io.BytesIO() @@ -609,7 +610,6 @@ def generate_csv_io(self, data: List[OfficeShortage], filename: str): return bytes_data - def save_csv_memory(self, data: List[object], filename: str, From f2fdb8eca12069b6f69fe4c3a453f2eb10e89d6e Mon Sep 17 00:00:00 2001 From: deymonster Date: Tue, 20 Feb 2024 16:39:28 +0500 Subject: [PATCH 08/28] Add postgresql service to save data to DB --- docker-compose.yaml | 19 ++ poetry.lock | 232 +++++++++++++++++- pyproject.toml | 2 + src/alembic.ini | 117 +++++++++ src/alembic/README | 1 + src/alembic/env.py | 90 +++++++ src/alembic/script.py.mako | 26 ++ src/alembic/versions/2b741b05d1a6_initial.py | 30 +++ .../ec308052fd0f_add_saleobject_table.py | 49 ++++ src/db/base_class.py | 30 +++ src/db/db.py | 15 ++ src/db/session.py | 13 +- src/parser/api.py | 5 +- src/parser/models.py | 27 ++ src/parser/schemas.py | 23 ++ src/parser/service.py | 64 +++++ 16 files changed, 733 insertions(+), 10 deletions(-) create mode 100644 src/alembic.ini create mode 100644 src/alembic/README create mode 100644 src/alembic/env.py create mode 100644 src/alembic/script.py.mako create mode 100644 src/alembic/versions/2b741b05d1a6_initial.py create mode 100644 src/alembic/versions/ec308052fd0f_add_saleobject_table.py create mode 100644 src/db/base_class.py create mode 100644 src/db/db.py create mode 100644 src/parser/models.py create mode 100644 src/parser/schemas.py create mode 100644 src/parser/service.py diff --git a/docker-compose.yaml b/docker-compose.yaml index 82b6571..2c8d30f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,18 @@ version: '3.8' services: + wb-db: + image: postgres:14-alpine + container_name: wb-db + restart: always + ports: + - "5432:5432" + volumes: + - wb-db-data:/var/lib/postgresql/data/pgdata + env_file: + - .env + environment: + PGDATA: /var/lib/postgresql/data/pgdata wb-parser-bot: build: context: . @@ -13,5 +25,12 @@ services: volumes: - ./src:/home/wb_user/bot restart: on-failure + depends_on: + - wb-db + +volumes: + wb-db-data: + + diff --git a/poetry.lock b/poetry.lock index f65222c..fd5bea8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,34 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "alembic" +version = "1.13.1" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, + {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] [[package]] name = "anyio" @@ -258,6 +288,94 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "mako" +version = "1.3.2" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, + {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "numpy" version = "1.23.2" @@ -422,6 +540,116 @@ files = [ {file = "psycopg2_binary-2.9.8-cp39-cp39-win_amd64.whl", hash = "sha256:1f279ba74f0d6b374526e5976c626d2ac3b8333b6a7b08755c513f4d380d3add"}, ] +[[package]] +name = "pydantic" +version = "2.6.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, + {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, + {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, + {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, + {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, + {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, + {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, + {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, + {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, + {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, + {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, + {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, + {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, + {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "python-dateutil" version = "2.8.2" @@ -660,4 +888,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.11.5" -content-hash = "dca2aa632622f2b2f3ad554b02134067cb66e0e75c832609d76c7c662b82dacf" +content-hash = "f4400d6f59c98014b1e5d667d4ccdf218a160b74280f14274ea673df20bc42e6" diff --git a/pyproject.toml b/pyproject.toml index c801146..84fb700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ schedule = "^1.2.0" python-telegram-bot = "^20.5" sqlalchemy = "^2.0.21" psycopg2-binary = "^2.9.8" +pydantic = "^2.6.1" +alembic = "^1.13.1" [build-system] diff --git a/src/alembic.ini b/src/alembic.ini new file mode 100644 index 0000000..1fe0d88 --- /dev/null +++ b/src/alembic.ini @@ -0,0 +1,117 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + + +sqlalchemy.url = postgresql://%(DB_USER)s:%(DB_PASSWORD)s@%(DB_HOST)s:%(DB_PORT)s/%(DB_NAME)s + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/src/alembic/README b/src/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/src/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/src/alembic/env.py b/src/alembic/env.py new file mode 100644 index 0000000..8d817d2 --- /dev/null +++ b/src/alembic/env.py @@ -0,0 +1,90 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context +from db.base_class import Base + +from utils.env import POSTGRES_SERVER, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_PORT +from parser.models import SaleObject + + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +section = config.config_ini_section +config.set_section_option(section, "DB_HOST", str(POSTGRES_SERVER)) +config.set_section_option(section, "DB_USER", str(POSTGRES_USER)) +config.set_section_option(section, "DB_PASSWORD", str(POSTGRES_PASSWORD)) +config.set_section_option(section, "DB_NAME", str(POSTGRES_DB)) +config.set_section_option(section, "DB_PORT", str(POSTGRES_PORT)) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/alembic/script.py.mako b/src/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/src/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/src/alembic/versions/2b741b05d1a6_initial.py b/src/alembic/versions/2b741b05d1a6_initial.py new file mode 100644 index 0000000..8021f9e --- /dev/null +++ b/src/alembic/versions/2b741b05d1a6_initial.py @@ -0,0 +1,30 @@ +"""Initial + +Revision ID: 2b741b05d1a6 +Revises: +Create Date: 2024-02-20 06:53:23.390172 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '2b741b05d1a6' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/src/alembic/versions/ec308052fd0f_add_saleobject_table.py b/src/alembic/versions/ec308052fd0f_add_saleobject_table.py new file mode 100644 index 0000000..25bbbef --- /dev/null +++ b/src/alembic/versions/ec308052fd0f_add_saleobject_table.py @@ -0,0 +1,49 @@ +"""Add SaleObject Table + +Revision ID: ec308052fd0f +Revises: 2b741b05d1a6 +Create Date: 2024-02-20 07:06:55.441322 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ec308052fd0f' +down_revision: Union[str, None] = '2b741b05d1a6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('saleobject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('office_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(), nullable=True), + sa.Column('date', sa.Date(), nullable=True), + sa.Column('sale_count', sa.Integer(), nullable=True), + sa.Column('return_count', sa.Integer(), nullable=True), + sa.Column('sale_sum', sa.Integer(), nullable=True), + sa.Column('return_sum', sa.Integer(), nullable=True), + sa.Column('proceeds', sa.Integer(), nullable=True), + sa.Column('amount', sa.Integer(), nullable=True), + sa.Column('bags_sum', sa.Integer(), nullable=True), + sa.Column('office_rating', sa.Float(), nullable=True), + sa.Column('percent', sa.Integer(), nullable=True), + sa.Column('office_rating_sum', sa.Integer(), nullable=True), + sa.Column('supplier_return_sum', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_saleobject_id'), 'saleobject', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_saleobject_id'), table_name='saleobject') + op.drop_table('saleobject') + # ### end Alembic commands ### diff --git a/src/db/base_class.py b/src/db/base_class.py new file mode 100644 index 0000000..32c17e2 --- /dev/null +++ b/src/db/base_class.py @@ -0,0 +1,30 @@ +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from pydantic import BaseModel + + +def is_pydantic(obj: object): + """Checks whether an object is pydantic.""" + return type(obj).__class__.__name__ == "ModelMetaclass" + + +class CustomBase: + # Generate __tablename__ automaticaly + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + @classmethod + def from_dto(cls, dto: BaseModel): + obj = cls() + properties = dict(dto) + for key, value in properties.items(): + try: + if is_pydantic(value): + value = getattr(cls, key).property.mapper.class_.from_dto(value) + setattr(obj, key, value) + except AttributeError as e: + raise AttributeError(e) + return obj + + +Base = declarative_base(cls=CustomBase) diff --git a/src/db/db.py b/src/db/db.py new file mode 100644 index 0000000..ca8344e --- /dev/null +++ b/src/db/db.py @@ -0,0 +1,15 @@ +from db.session import SessionLocal +from contextlib import contextmanager + + +@contextmanager +def get_db(): + db = SessionLocal() + try: + yield db + db.commit() + except Exception as e: + db.rollback() + raise + finally: + db.close() diff --git a/src/db/session.py b/src/db/session.py index f9b6dd4..82e63e1 100644 --- a/src/db/session.py +++ b/src/db/session.py @@ -1,9 +1,8 @@ -# from sqlalchemy import create_engine -# from sqlalchemy.orm import scoped_session, sessionmaker -# from src.utils import env -# -# engine = create_engine(env.DATABASE_URL, pool_pre_ping=True) -# db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) -# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker +from utils import env +engine = create_engine(env.DATABASE_URL, pool_pre_ping=True) +db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/src/parser/api.py b/src/parser/api.py index 73dbc8d..a5bcd9c 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -1,6 +1,8 @@ import csv from typing import Dict, Any, Callable, Union, IO import io + +from parser.service import safe_sale_object_to_db from utils.env import base_url_v2, base_url_v1, refresh_url, operations_url, shortages_url, shks_url from datetime import datetime, timedelta import pandas as pd @@ -517,7 +519,8 @@ def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ logger.info(f'Обработана дата {date} для офиса {office_id} -- {sale_object.name}') result.append(sale_object) - + logger.info('Begin to write data to DB') + safe_sale_object_to_db(sale_objects=result) return result def fetch_operations_data(self, date_from: datetime = None, date_to: datetime = None): diff --git a/src/parser/models.py b/src/parser/models.py new file mode 100644 index 0000000..78fd4f6 --- /dev/null +++ b/src/parser/models.py @@ -0,0 +1,27 @@ +from db.base_class import Base +from sqlalchemy import Column, Integer, String, Date, DateTime, Float + + +class SaleObject(Base): + """Model poll""" + + id = Column(Integer, primary_key=True, index=True) + office_id = Column(Integer) + name = Column(String) + date = Column(Date) + sale_count = Column(Integer) + return_count = Column(Integer) + sale_sum = Column(Integer) + return_sum = Column(Integer) + proceeds = Column(Integer) + amount = Column(Integer) + bags_sum = Column(Integer) + office_rating = Column(Float) + percent = Column(Integer) + office_rating_sum = Column(Integer) + supplier_return_sum = Column(Integer) + + + + + diff --git a/src/parser/schemas.py b/src/parser/schemas.py new file mode 100644 index 0000000..8e97273 --- /dev/null +++ b/src/parser/schemas.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class SaleObjectIn(BaseModel): + """Schema for SaleObject in DB""" + office_id: int + name: str + date: datetime + sale_count: int + return_count: int + sale_sum: int + return_sum: int + proceeds: int + amount: int + bags_sum: int + office_rating: float + percent: int + office_rating_sum: int + supplier_return_sum: int + + diff --git a/src/parser/service.py b/src/parser/service.py new file mode 100644 index 0000000..86e32ed --- /dev/null +++ b/src/parser/service.py @@ -0,0 +1,64 @@ +from typing import List + +from pydantic import ValidationError + +from db.db import get_db + +from parser.schemas import SaleObjectIn +from parser.models import SaleObject +from sqlalchemy.orm import Session +from sqlalchemy.orm.exc import NoResultFound +from bot.logger import WBLogger + +logger = WBLogger(__name__).get_logger() + + +def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, return_count, sale_sum, return_sum, + proceeds, amount, bags_sum, office_rating, percent, office_rating_sum, + supplier_return_sum): + return SaleObjectIn( + office_id=office_id, + date=date, + name=name, + sale_count=sale_count, + return_count=return_count, + sale_sum=int(sale_sum), + return_sum=return_sum, + proceeds=int(proceeds), + amount=int(amount), + bags_sum=bags_sum, + office_rating=int(office_rating), + percent=int(percent), + office_rating_sum=int(office_rating_sum), + supplier_return_sum=supplier_return_sum, + ) + + +def get_or_none(session: Session, model, **kwargs): + """ Функция для проверки наличие данных в БД""" + try: + return session.query(model).filter_by(**kwargs).first() + except NoResultFound: + return None + + +def safe_sale_object_to_db(sale_objects: List[dict]): + """ Сервис функция для валидации входных данных - списка SaleObjectIn и запись их в БД""" + for sale_object in sale_objects: + with get_db() as db: + try: + logger.info(f'sale_object is - {sale_object}') + sale_object_in = convert_sale_data_to_sale_object_in(**sale_object.to_dict()) + existing_data = get_or_none(db, SaleObject, date=sale_object_in.date, + office_id=sale_object_in.office_id) + if existing_data: + logger.info( + f"Данные для даты {sale_object_in.date} " + f"и офиса {sale_object_in.office_id} уже существуют. Пропускаем.") + continue + db_data = SaleObject(**sale_object_in.model_dump()) + db.add(db_data) + logger.info(f"Данные записаны - {db_data}") + except ValidationError as e: + logger.error(f"Ошибка валидации данных: {e}") + continue From fe10e26fb3f97cae096d4ecbea5fd6fa9ca7deb2 Mon Sep 17 00:00:00 2001 From: deymonster Date: Tue, 20 Feb 2024 16:51:05 +0500 Subject: [PATCH 09/28] Add star.sh with alembic upgrade --- docker-compose.yaml | 2 +- src/start.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/start.sh diff --git a/docker-compose.yaml b/docker-compose.yaml index 2c8d30f..705b45c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,7 +19,7 @@ services: dockerfile: Dockerfile container_name: wb-parser-bot image: wb-parser-bot:latest - command: ["python3", "main_bot.py"] + command: bash start.sh env_file: - .env volumes: diff --git a/src/start.sh b/src/start.sh new file mode 100644 index 0000000..42b1bd1 --- /dev/null +++ b/src/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +# Apply Alembic migrations +alembic upgrade head +# Start Bot +python3 main_bot.py \ No newline at end of file From 189a0b909fd8f631b1f026771c644903c4d731f0 Mon Sep 17 00:00:00 2001 From: deymonster Date: Tue, 20 Feb 2024 16:54:17 +0500 Subject: [PATCH 10/28] Convert return_sum to int --- src/parser/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/service.py b/src/parser/service.py index 86e32ed..a934fbb 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -23,7 +23,7 @@ def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, retur sale_count=sale_count, return_count=return_count, sale_sum=int(sale_sum), - return_sum=return_sum, + return_sum=int(return_sum), proceeds=int(proceeds), amount=int(amount), bags_sum=bags_sum, From 4a728ade6aa106e7ed81ec4ddcc62b082d794244 Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 21 Feb 2024 11:45:28 +0500 Subject: [PATCH 11/28] Change office_rating and percent to float, add round in service --- ...17_change_office_rating_and_percent_to_.py | 36 +++++++++++++++++++ src/parser/models.py | 2 +- src/parser/schemas.py | 2 +- src/parser/service.py | 4 +-- 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py diff --git a/src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py b/src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py new file mode 100644 index 0000000..f706c9b --- /dev/null +++ b/src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py @@ -0,0 +1,36 @@ +"""Change office_rating and percent to float and round + +Revision ID: eab151538217 +Revises: ec308052fd0f +Create Date: 2024-02-21 06:44:05.784419 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eab151538217' +down_revision: Union[str, None] = 'ec308052fd0f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('saleobject', 'percent', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('saleobject', 'percent', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=True) + # ### end Alembic commands ### diff --git a/src/parser/models.py b/src/parser/models.py index 78fd4f6..9e75dcb 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -17,7 +17,7 @@ class SaleObject(Base): amount = Column(Integer) bags_sum = Column(Integer) office_rating = Column(Float) - percent = Column(Integer) + percent = Column(Float) office_rating_sum = Column(Integer) supplier_return_sum = Column(Integer) diff --git a/src/parser/schemas.py b/src/parser/schemas.py index 8e97273..acfe91f 100644 --- a/src/parser/schemas.py +++ b/src/parser/schemas.py @@ -16,7 +16,7 @@ class SaleObjectIn(BaseModel): amount: int bags_sum: int office_rating: float - percent: int + percent: float office_rating_sum: int supplier_return_sum: int diff --git a/src/parser/service.py b/src/parser/service.py index a934fbb..4781ea1 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -27,8 +27,8 @@ def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, retur proceeds=int(proceeds), amount=int(amount), bags_sum=bags_sum, - office_rating=int(office_rating), - percent=int(percent), + office_rating=round(float(office_rating), 3), + percent=round(float(percent), 2), office_rating_sum=int(office_rating_sum), supplier_return_sum=supplier_return_sum, ) From 3d537635d3520f7602cb350b554ccad06acbb373 Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 28 Feb 2024 10:17:46 +0500 Subject: [PATCH 12/28] Add office model with init data, update sale model --- Dockerfile | 3 ++ docker-compose.yaml | 2 + ...add_saleobject_and_officeobject_tables.py} | 36 ++++++++++++-- .../c14acaa13ef7_updated_saleobject_model.py | 48 +++++++++++++++++++ ...17_change_office_rating_and_percent_to_.py | 36 -------------- src/initial_data.py | 43 +++++++++++++++++ src/parser/api.py | 28 +++++------ src/parser/models.py | 29 +++++++++++ src/parser/service.py | 40 +++++++++++++++- src/start.sh | 2 + 10 files changed, 210 insertions(+), 57 deletions(-) rename src/alembic/versions/{ec308052fd0f_add_saleobject_table.py => 192266bd83e7_add_saleobject_and_officeobject_tables.py} (50%) create mode 100644 src/alembic/versions/c14acaa13ef7_updated_saleobject_model.py delete mode 100644 src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py create mode 100644 src/initial_data.py diff --git a/Dockerfile b/Dockerfile index 0757df4..6cf928b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,9 @@ RUN useradd -m -d /home/${USER} ${USER} \ RUN mkdir -p ${PROJECTPATH} +ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait ${PROJECTPATH}/wait +RUN chmod +x ${PROJECTPATH}/wait + RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 - \ && cd /usr/local/bin \ && ln -s /etc/poetry/bin/poetry \ diff --git a/docker-compose.yaml b/docker-compose.yaml index 705b45c..a77fad0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,8 @@ services: command: bash start.sh env_file: - .env + environment: + WAIT-HOSTS: "wb-db" volumes: - ./src:/home/wb_user/bot restart: on-failure diff --git a/src/alembic/versions/ec308052fd0f_add_saleobject_table.py b/src/alembic/versions/192266bd83e7_add_saleobject_and_officeobject_tables.py similarity index 50% rename from src/alembic/versions/ec308052fd0f_add_saleobject_table.py rename to src/alembic/versions/192266bd83e7_add_saleobject_and_officeobject_tables.py index 25bbbef..9e317fd 100644 --- a/src/alembic/versions/ec308052fd0f_add_saleobject_table.py +++ b/src/alembic/versions/192266bd83e7_add_saleobject_and_officeobject_tables.py @@ -1,8 +1,8 @@ -"""Add SaleObject Table +"""Add saleobject and officeobject tables -Revision ID: ec308052fd0f +Revision ID: 192266bd83e7 Revises: 2b741b05d1a6 -Create Date: 2024-02-20 07:06:55.441322 +Create Date: 2024-02-27 09:09:45.554227 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = 'ec308052fd0f' +revision: str = '192266bd83e7' down_revision: Union[str, None] = '2b741b05d1a6' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -20,9 +20,26 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.create_table('officeobject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('office_id', sa.Integer(), nullable=True), + sa.Column('company', sa.String(), nullable=True), + sa.Column('manager', sa.String(), nullable=True), + sa.Column('office_area', sa.Integer(), nullable=True), + sa.Column('rent', sa.Float(), nullable=True), + sa.Column('salary_rate', sa.Float(), nullable=True), + sa.Column('min_wage', sa.Integer(), nullable=True), + sa.Column('internet', sa.Integer(), nullable=True), + sa.Column('administration', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('office_id') + ) + op.create_index(op.f('ix_officeobject_id'), 'officeobject', ['id'], unique=False) op.create_table('saleobject', sa.Column('id', sa.Integer(), nullable=False), sa.Column('office_id', sa.Integer(), nullable=True), + sa.Column('company', sa.String(), nullable=True), + sa.Column('manager', sa.String(), nullable=True), sa.Column('name', sa.String(), nullable=True), sa.Column('date', sa.Date(), nullable=True), sa.Column('sale_count', sa.Integer(), nullable=True), @@ -33,9 +50,16 @@ def upgrade() -> None: sa.Column('amount', sa.Integer(), nullable=True), sa.Column('bags_sum', sa.Integer(), nullable=True), sa.Column('office_rating', sa.Float(), nullable=True), - sa.Column('percent', sa.Integer(), nullable=True), + sa.Column('percent', sa.Float(), nullable=True), sa.Column('office_rating_sum', sa.Integer(), nullable=True), sa.Column('supplier_return_sum', sa.Integer(), nullable=True), + sa.Column('reward_plan', sa.Float(), nullable=True), + sa.Column('salary_fund_plan', sa.Float(), nullable=True), + sa.Column('actual_salary_fund', sa.Float(), nullable=True), + sa.Column('difference_salary_fund', sa.Float(), nullable=True), + sa.Column('rent', sa.Float(), nullable=True), + sa.Column('administration', sa.Integer(), nullable=True), + sa.Column('internet', sa.Integer(), nullable=True), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_saleobject_id'), 'saleobject', ['id'], unique=False) @@ -46,4 +70,6 @@ def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_saleobject_id'), table_name='saleobject') op.drop_table('saleobject') + op.drop_index(op.f('ix_officeobject_id'), table_name='officeobject') + op.drop_table('officeobject') # ### end Alembic commands ### diff --git a/src/alembic/versions/c14acaa13ef7_updated_saleobject_model.py b/src/alembic/versions/c14acaa13ef7_updated_saleobject_model.py new file mode 100644 index 0000000..398cb90 --- /dev/null +++ b/src/alembic/versions/c14acaa13ef7_updated_saleobject_model.py @@ -0,0 +1,48 @@ +"""Updated SaleObject model + +Revision ID: c14acaa13ef7 +Revises: 192266bd83e7 +Create Date: 2024-02-28 04:35:37.858185 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c14acaa13ef7' +down_revision: Union[str, None] = '192266bd83e7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('saleobject', sa.Column('maintenance', sa.Float(), nullable=True)) + op.add_column('saleobject', sa.Column('profitability', sa.Float(), nullable=True)) + op.alter_column('saleobject', 'administration', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=True) + op.alter_column('saleobject', 'internet', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('saleobject', 'internet', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=True) + op.alter_column('saleobject', 'administration', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=True) + op.drop_column('saleobject', 'profitability') + op.drop_column('saleobject', 'maintenance') + # ### end Alembic commands ### diff --git a/src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py b/src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py deleted file mode 100644 index f706c9b..0000000 --- a/src/alembic/versions/eab151538217_change_office_rating_and_percent_to_.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Change office_rating and percent to float and round - -Revision ID: eab151538217 -Revises: ec308052fd0f -Create Date: 2024-02-21 06:44:05.784419 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'eab151538217' -down_revision: Union[str, None] = 'ec308052fd0f' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('saleobject', 'percent', - existing_type=sa.INTEGER(), - type_=sa.Float(), - existing_nullable=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('saleobject', 'percent', - existing_type=sa.Float(), - type_=sa.INTEGER(), - existing_nullable=True) - # ### end Alembic commands ### diff --git a/src/initial_data.py b/src/initial_data.py new file mode 100644 index 0000000..727f7b0 --- /dev/null +++ b/src/initial_data.py @@ -0,0 +1,43 @@ +from bot.logger import WBLogger +from db.session import SessionLocal +import csv + +from parser.models import OfficeObject + +# set up logging +logger = WBLogger(__name__).get_logger() + + +with open('data.csv', 'r', encoding='utf-8-sig') as file: + csv_reader = csv.DictReader(file, fieldnames=['office_id', 'company', 'manager', 'office_area', 'rent', + 'salary_rate', 'min_wage', 'internet', 'administration'], + ) + next(csv_reader) + for row in csv_reader: + logger.info(f'row - {row}') + office_id = int(row.get('office_id', 0)) + company = row.get('company', '') + manager = row.get('manager', '') + office_area = int(row['office_area']) if row['office_area'] else 0 + rent = float(row.get('rent', 0.0)) + salary_rate = float(row.get('salary_rate', 0.0)) + min_wage = int(row.get('min_wage', 0)) + internet = int(row.get('internet', 0)) + administration = int(row.get('administration', 0)) + + office_object = OfficeObject( + office_id=office_id, + company=company, + manager=manager, + office_area=office_area, + rent=rent, + salary_rate=salary_rate, + min_wage=min_wage, + internet=internet, + administration=administration + ) + db = SessionLocal() + db.merge(office_object) + logger.info(f'Office ID {office_object.office_id} was added') + db.commit() +logger.info('Applied initial office data') diff --git a/src/parser/api.py b/src/parser/api.py index a5bcd9c..4e8e5c6 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -248,20 +248,20 @@ class SaleData: """Класс для хранения данных о продажах""" def __init__(self, - office_id: int, - name: str, - date: datetime, - sale_sum: int, - sale_count: int, - return_sum: int, - return_count: int, - proceeds: int, - amount: int, - bags_sum: int, - office_rating: float, - percent: int, - office_rating_sum: int, - supplier_return_sum: int, + office_id: int, # id офиса + name: str, # наименование офиса + date: datetime, # дата + sale_sum: int, # продажи + sale_count: int, # количество продаж + return_sum: int, # возвраты + return_count: int, # количество возвратов + proceeds: int, # вознаграждения + amount: int, # объем продаж + bags_sum: int, # пакеты + office_rating: float, # рейтинг ПВЗ + percent: int, # тариф ставка грейд + office_rating_sum: int, # сумма рейтинга + supplier_return_sum: int, # сумма возвратов ): self.office_id = office_id self.name = name diff --git a/src/parser/models.py b/src/parser/models.py index 9e75dcb..eeedf83 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -2,11 +2,28 @@ from sqlalchemy import Column, Integer, String, Date, DateTime, Float +class OfficeObject(Base): + """Office object""" + + id = Column(Integer, primary_key=True, index=True) + office_id = Column(Integer, unique=True) + company = Column(String) + manager = Column(String) + office_area = Column(Integer, default=0) + rent = Column(Float) + salary_rate = Column(Float) + min_wage = Column(Integer) + internet = Column(Integer) + administration = Column(Integer) + + class SaleObject(Base): """Model poll""" id = Column(Integer, primary_key=True, index=True) office_id = Column(Integer) + company = Column(String) + manager = Column(String) name = Column(String) date = Column(Date) sale_count = Column(Integer) @@ -21,6 +38,18 @@ class SaleObject(Base): office_rating_sum = Column(Integer) supplier_return_sum = Column(Integer) + reward_plan = Column(Float) + salary_fund_plan = Column(Float) + actual_salary_fund = Column(Float) + difference_salary_fund = Column(Float) + rent = Column(Float) + administration = Column(Float) + internet = Column(Float) + + maintenance = Column(Float) + profitability = Column(Float) + + diff --git a/src/parser/service.py b/src/parser/service.py index 4781ea1..069de65 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -5,7 +5,7 @@ from db.db import get_db from parser.schemas import SaleObjectIn -from parser.models import SaleObject +from parser.models import SaleObject, OfficeObject from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound from bot.logger import WBLogger @@ -42,6 +42,13 @@ def get_or_none(session: Session, model, **kwargs): return None +def get_office_info(session: Session, office_id): + """Получение офиса констант по office_id""" + + office_data = session.query(OfficeObject).filter_by(office_id=office_id).first() + return office_data if office_data else None + + def safe_sale_object_to_db(sale_objects: List[dict]): """ Сервис функция для валидации входных данных - списка SaleObjectIn и запись их в БД""" for sale_object in sale_objects: @@ -56,7 +63,36 @@ def safe_sale_object_to_db(sale_objects: List[dict]): f"Данные для даты {sale_object_in.date} " f"и офиса {sale_object_in.office_id} уже существуют. Пропускаем.") continue - db_data = SaleObject(**sale_object_in.model_dump()) + # office_data = get_office_info(db, sale_object_in.office_id) + office_data = get_or_none(db, OfficeObject, office_id=sale_object_in.office_id) + logger.info(f'Office data - {office_data}') + # Вычисление дополнительных полей + reward_plan = round(float(sale_object_in.sale_sum * sale_object_in.percent), 2) + salary_fund_plan = round(float(sale_object_in.sale_sum * office_data.salary_rate), 2) + actual_salary_fund = round(float(max(salary_fund_plan, office_data.min_wage)), 2) + difference_salary_fund = salary_fund_plan - actual_salary_fund + daily_rent = round(float(office_data.rent / 30), 2) + daily_administration = round(float(office_data.administration / 30), 2) + daily_internet = office_data.internet / 30 + + maintenance = round(float(sale_object_in.proceeds / (1000000 * 630)), 2) + profitability = round( + float(reward_plan - actual_salary_fund - daily_administration - daily_internet - maintenance), 2) + + db_data = SaleObject( + **sale_object_in.model_dump(), + company=office_data.company, + manager=office_data.manager, + reward_plan=reward_plan, + salary_fund_plan=salary_fund_plan, + actual_salary_fund=actual_salary_fund, + difference_salary_fund=difference_salary_fund, + rent=daily_rent, + administration=daily_administration, + internet=daily_internet, + maintenance=maintenance, + profitability=profitability + ) db.add(db_data) logger.info(f"Данные записаны - {db_data}") except ValidationError as e: diff --git a/src/start.sh b/src/start.sh index 42b1bd1..eb53c15 100644 --- a/src/start.sh +++ b/src/start.sh @@ -4,5 +4,7 @@ set -e # Apply Alembic migrations alembic upgrade head +# Apply initial data +python3 initial_data.py # Start Bot python3 main_bot.py \ No newline at end of file From 084e93136b69f4dc707c86cb7d22493a764319fa Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 28 Feb 2024 10:55:54 +0500 Subject: [PATCH 13/28] Update calculating fields of saleobject model --- src/parser/service.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/parser/service.py b/src/parser/service.py index 069de65..a5e5c3e 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -67,15 +67,15 @@ def safe_sale_object_to_db(sale_objects: List[dict]): office_data = get_or_none(db, OfficeObject, office_id=sale_object_in.office_id) logger.info(f'Office data - {office_data}') # Вычисление дополнительных полей - reward_plan = round(float(sale_object_in.sale_sum * sale_object_in.percent), 2) - salary_fund_plan = round(float(sale_object_in.sale_sum * office_data.salary_rate), 2) + reward_plan = round(float(sale_object_in.proceeds * sale_object_in.percent / 100), 2) + salary_fund_plan = round(float(sale_object_in.proceeds * office_data.salary_rate / 100), 2) actual_salary_fund = round(float(max(salary_fund_plan, office_data.min_wage)), 2) - difference_salary_fund = salary_fund_plan - actual_salary_fund + difference_salary_fund = actual_salary_fund - salary_fund_plan daily_rent = round(float(office_data.rent / 30), 2) daily_administration = round(float(office_data.administration / 30), 2) - daily_internet = office_data.internet / 30 + daily_internet = round(float(office_data.internet / 30), 2) - maintenance = round(float(sale_object_in.proceeds / (1000000 * 630)), 2) + maintenance = round(float(sale_object_in.proceeds / 1000000 * 630), 2) profitability = round( float(reward_plan - actual_salary_fund - daily_administration - daily_internet - maintenance), 2) From 1c2c4a43237543b793ac3073147ec2a8ad016532 Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 29 Feb 2024 15:41:51 +0500 Subject: [PATCH 14/28] Add new data field to DB - office_speed_sum --- ...dd_office_speed_sum_field_to_saleobject.py | 30 +++++++++++++++++++ src/parser/api.py | 4 +++ src/parser/models.py | 1 + src/parser/schemas.py | 1 + src/parser/service.py | 5 ++-- 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/alembic/versions/84153a08c3db_add_office_speed_sum_field_to_saleobject.py diff --git a/src/alembic/versions/84153a08c3db_add_office_speed_sum_field_to_saleobject.py b/src/alembic/versions/84153a08c3db_add_office_speed_sum_field_to_saleobject.py new file mode 100644 index 0000000..04d4db0 --- /dev/null +++ b/src/alembic/versions/84153a08c3db_add_office_speed_sum_field_to_saleobject.py @@ -0,0 +1,30 @@ +"""Add office_speed_sum field to SaleObject + +Revision ID: 84153a08c3db +Revises: c14acaa13ef7 +Create Date: 2024-02-29 15:13:42.637241 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '84153a08c3db' +down_revision: Union[str, None] = 'c14acaa13ef7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('saleobject', sa.Column('office_speed_sum', sa.Float(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('saleobject', 'office_speed_sum') + # ### end Alembic commands ### diff --git a/src/parser/api.py b/src/parser/api.py index 4e8e5c6..bf7d8f5 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -262,6 +262,7 @@ def __init__(self, percent: int, # тариф ставка грейд office_rating_sum: int, # сумма рейтинга supplier_return_sum: int, # сумма возвратов + office_speed_sum: float # скорость ): self.office_id = office_id self.name = name @@ -277,6 +278,7 @@ def __init__(self, self.percent = percent self.office_rating_sum = office_rating_sum self.supplier_return_sum = supplier_return_sum + self.office_speed_sum = office_speed_sum def to_dict(self): return self.__dict__ @@ -498,6 +500,7 @@ def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ percent = reward_data['ext_data']['percent'][0] if reward_data else 0 office_rating_sum = reward_data['ext_data']['office_rating_sum'] if reward_data else 0 supplier_return_sum = reward_data['ext_data']['supplier_return_sum'] if reward_data else 0 + office_speed_sum = reward_data['ext_data']['office_speed_sum'] if reward_data else 0 sale_object = SaleData( office_id=office_id, @@ -514,6 +517,7 @@ def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ percent=percent, office_rating_sum=office_rating_sum, supplier_return_sum=supplier_return_sum, + office_speed_sum=office_speed_sum ) diff --git a/src/parser/models.py b/src/parser/models.py index eeedf83..347b273 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -37,6 +37,7 @@ class SaleObject(Base): percent = Column(Float) office_rating_sum = Column(Integer) supplier_return_sum = Column(Integer) + office_speed_sum = Column(Float) reward_plan = Column(Float) salary_fund_plan = Column(Float) diff --git a/src/parser/schemas.py b/src/parser/schemas.py index acfe91f..ecc3209 100644 --- a/src/parser/schemas.py +++ b/src/parser/schemas.py @@ -19,5 +19,6 @@ class SaleObjectIn(BaseModel): percent: float office_rating_sum: int supplier_return_sum: int + office_speed_sum: float diff --git a/src/parser/service.py b/src/parser/service.py index a5e5c3e..e3e68b1 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -15,7 +15,7 @@ def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, return_count, sale_sum, return_sum, proceeds, amount, bags_sum, office_rating, percent, office_rating_sum, - supplier_return_sum): + supplier_return_sum, office_speed_sum): return SaleObjectIn( office_id=office_id, date=date, @@ -31,6 +31,7 @@ def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, retur percent=round(float(percent), 2), office_rating_sum=int(office_rating_sum), supplier_return_sum=supplier_return_sum, + office_speed_sum=round(float(office_speed_sum), 2) ) @@ -70,7 +71,7 @@ def safe_sale_object_to_db(sale_objects: List[dict]): reward_plan = round(float(sale_object_in.proceeds * sale_object_in.percent / 100), 2) salary_fund_plan = round(float(sale_object_in.proceeds * office_data.salary_rate / 100), 2) actual_salary_fund = round(float(max(salary_fund_plan, office_data.min_wage)), 2) - difference_salary_fund = actual_salary_fund - salary_fund_plan + difference_salary_fund = round(float(actual_salary_fund - salary_fund_plan), 2), daily_rent = round(float(office_data.rent / 30), 2) daily_administration = round(float(office_data.administration / 30), 2) daily_internet = round(float(office_data.internet / 30), 2) From f737836cdaa876feb79fd131dd5286a61688ca2a Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 29 Feb 2024 17:16:38 +0500 Subject: [PATCH 15/28] Added checking office_speed_sum --- src/parser/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/api.py b/src/parser/api.py index bf7d8f5..3b5475d 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -501,7 +501,8 @@ def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ office_rating_sum = reward_data['ext_data']['office_rating_sum'] if reward_data else 0 supplier_return_sum = reward_data['ext_data']['supplier_return_sum'] if reward_data else 0 office_speed_sum = reward_data['ext_data']['office_speed_sum'] if reward_data else 0 - + if office_speed_sum is None: + office_speed_sum = 0 sale_object = SaleData( office_id=office_id, name=sales_data['office_name'], From 16cda2a47ac65bf7ae7db407be8e3190f9462fd0 Mon Sep 17 00:00:00 2001 From: deymonster Date: Sat, 2 Mar 2024 12:41:48 +0500 Subject: [PATCH 16/28] Update profitability, add office_speed_sum to report csv --- src/parser/column_names.py | 3 ++- src/parser/service.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/parser/column_names.py b/src/parser/column_names.py index 04f6144..27a0b36 100644 --- a/src/parser/column_names.py +++ b/src/parser/column_names.py @@ -13,7 +13,8 @@ 'office_rating': 'Рейтинг ПВЗ', 'percent': 'Тариф (ставка грейда)', 'office_rating_sum': 'Сумма рейтинга', - 'supplier_return_sum': 'Сумма возвратов поставщика' + 'supplier_return_sum': 'Сумма возвратов поставщика', + 'office_speed_sum': 'Скорость' } diff --git a/src/parser/service.py b/src/parser/service.py index e3e68b1..9be2166 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -78,7 +78,8 @@ def safe_sale_object_to_db(sale_objects: List[dict]): maintenance = round(float(sale_object_in.proceeds / 1000000 * 630), 2) profitability = round( - float(reward_plan - actual_salary_fund - daily_administration - daily_internet - maintenance), 2) + float(reward_plan - actual_salary_fund - + daily_administration - daily_internet - maintenance - daily_rent), 2) db_data = SaleObject( **sale_object_in.model_dump(), From 9267e6674dd2b774b2a433c426f133d55ad88732 Mon Sep 17 00:00:00 2001 From: deymonster Date: Mon, 4 Mar 2024 10:05:04 +0500 Subject: [PATCH 17/28] Add logging for office data and skip if none --- src/parser/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/parser/service.py b/src/parser/service.py index 9be2166..3bf442b 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -66,6 +66,9 @@ def safe_sale_object_to_db(sale_objects: List[dict]): continue # office_data = get_office_info(db, sale_object_in.office_id) office_data = get_or_none(db, OfficeObject, office_id=sale_object_in.office_id) + if not office_data: + logger.warning(f"Отсутствуют данные для офиса {sale_object_in.office_id}. Пропускаем.") + continue logger.info(f'Office data - {office_data}') # Вычисление дополнительных полей reward_plan = round(float(sale_object_in.proceeds * sale_object_in.percent / 100), 2) From 82d8d72b39268f33aa1f848a77f9da1d4f69d496 Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 21 Mar 2024 16:45:34 +0500 Subject: [PATCH 18/28] Add offices commands to work with them --- poetry.lock | 987 +++++++++--------- pyproject.toml | 2 +- ...office_area_in_officeobject_changed_to_.py | 36 + .../afa9f3aca061_add_name_to_officeobject.py | 30 + src/convert_csv.py | 18 + src/db/base_class.py | 1 + src/initial_data.py | 78 +- src/main_bot.py | 395 ++++++- src/parser/constants.py | 12 + src/parser/models.py | 3 +- src/parser/schemas.py | 16 +- src/parser/service.py | 93 +- src/utils/env.py | 1 + 13 files changed, 1157 insertions(+), 515 deletions(-) create mode 100644 src/alembic/versions/94c08ee5ba68_office_area_in_officeobject_changed_to_.py create mode 100644 src/alembic/versions/afa9f3aca061_add_name_to_officeobject.py create mode 100644 src/convert_csv.py create mode 100644 src/parser/constants.py diff --git a/poetry.lock b/poetry.lock index fd5bea8..7d36a49 100644 --- a/poetry.lock +++ b/poetry.lock @@ -32,13 +32,13 @@ files = [ [[package]] name = "anyio" -version = "4.0.0" +version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [package.dependencies] @@ -46,180 +46,189 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.7" files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] @@ -235,39 +244,40 @@ files = [ [[package]] name = "httpcore" -version = "0.17.3" +version = "1.0.4" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, - {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, ] [package.dependencies] -anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] [[package]] name = "httpx" -version = "0.24.1" +version = "0.27.0" description = "The next generation HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, - {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.15.0,<0.18.0" +httpcore = "==1.*" idna = "*" sniffio = "*" @@ -279,13 +289,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -378,182 +388,216 @@ files = [ [[package]] name = "numpy" -version = "1.23.2" -description = "NumPy is the fundamental package for array computing with Python." +version = "1.26.4" +description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "numpy-1.23.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5"}, - {file = "numpy-1.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8"}, - {file = "numpy-1.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0"}, - {file = "numpy-1.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5"}, - {file = "numpy-1.23.2-cp310-cp310-win32.whl", hash = "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450"}, - {file = "numpy-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414"}, - {file = "numpy-1.23.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c"}, - {file = "numpy-1.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524"}, - {file = "numpy-1.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde"}, - {file = "numpy-1.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418"}, - {file = "numpy-1.23.2-cp311-cp311-win32.whl", hash = "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722"}, - {file = "numpy-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e"}, - {file = "numpy-1.23.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f"}, - {file = "numpy-1.23.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842"}, - {file = "numpy-1.23.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215"}, - {file = "numpy-1.23.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66"}, - {file = "numpy-1.23.2-cp38-cp38-win32.whl", hash = "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d"}, - {file = "numpy-1.23.2-cp38-cp38-win_amd64.whl", hash = "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d"}, - {file = "numpy-1.23.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0"}, - {file = "numpy-1.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6"}, - {file = "numpy-1.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074"}, - {file = "numpy-1.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77"}, - {file = "numpy-1.23.2-cp39-cp39-win32.whl", hash = "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c"}, - {file = "numpy-1.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913"}, - {file = "numpy-1.23.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38"}, - {file = "numpy-1.23.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f"}, - {file = "numpy-1.23.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389"}, - {file = "numpy-1.23.2.tar.gz", hash = "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] name = "pandas" -version = "2.1.0" +version = "2.2.1" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, - {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, - {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, - {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, - {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, - {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, - {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, - {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, - {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, + {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, + {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, + {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, + {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, + {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, + {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, + {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, ] [package.dependencies] -numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""} +numpy = [ + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, +] python-dateutil = ">=2.8.2" pytz = ">=2020.1" -tzdata = ">=2022.1" +tzdata = ">=2022.7" [package.extras] -all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] -aws = ["s3fs (>=2022.05.0)"] -clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] -compression = ["zstandard (>=0.17.0)"] -computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2022.05.0)"] -gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] -hdf5 = ["tables (>=3.7.0)"] -html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] -mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] -spss = ["pyreadstat (>=1.1.5)"] -sql-other = ["SQLAlchemy (>=1.4.36)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.8.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "psycopg2-binary" -version = "2.9.8" +version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "psycopg2-binary-2.9.8.tar.gz", hash = "sha256:80451e6b6b7c486828d5c7ed50769532bbb04ec3a411f1e833539d5c10eb691c"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e271ad6692d50d70ca75db3bd461bfc26316de78de8fe1f504ef16dcea8f2312"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ae22a0fa5c516b84ddb189157fabfa3f12eded5d630e1ce260a18e1771f8707"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a971086db0069aef2fd22ccffb670baac427f4ee2174c4f5c7206254f1e6794"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b6928a502af71ca2ac9aad535e78c8309892ed3bfa7933182d4c760580c8af4"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f955fe6301b84b6fd13970a05f3640fbb62ca3a0d19342356585006c830e038"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3723c3f009e2b2771f2491b330edb7091846f1aad0c08fbbd9a1383d6a0c0841"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e3142c7e51b92855cff300580de949e36a94ab3bfa8f353b27fe26535e9b3542"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:de85105c568dc5f0f0efe793209ba83e4675d53d00faffc7a7c7a8bea9e0e19a"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c7ff2b6a79a92b1b169b03bb91b41806843f0cdf6055256554495bffed1d496d"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59f45cca0765aabb52a5822c72d5ff2ec46a28b1c1702de90dc0d306ec5c2001"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-win32.whl", hash = "sha256:1dbad789ebd1e61201256a19dc2e90fed4706bc966ccad4f374648e5336b1ab4"}, - {file = "psycopg2_binary-2.9.8-cp310-cp310-win_amd64.whl", hash = "sha256:15458c81b0d199ab55825007115f697722831656e6477a427783fe75c201c82b"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:395c217156723fe21809dfe8f7a433c5bf8e9bce229944668e4ec709c37c5442"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14f85ff2d5d826a7ce9e6c31e803281ed5a096789f47f52cb728c88f488de01b"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e11373d8e4f1f46cf3065bf613f0df9854803dc95aa4a35354ffac19f8c52127"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01f9731761f711e42459f87bd2ad5d744b9773b5dd05446f3b579a0f077e78e3"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bf5c27bd5867a5fa5341fad29f0d5838e2fed617ef5346884baf8b8b16dd82"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfabbd7e70785af726cc0209e8e64b926abf91741eca80678b221aad9e72135"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6369f4bd4d27944498094dccced1ae7ca43376a59dbfe4c8b6a16e9e3dc3ccce"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4879ee1d07a6b2c232ae6a74570f4788cd7a29b3cd38bc39bf60225b1d075c78"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4336afc0e81726350bd5863e3c3116d8c12aa7f457d3d0b3b3dc36137fec6feb"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:63ce1dccfd08d9c5341ac82d62aa04345bc4bf41b5e5b7b2c6c172a28e0eda27"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-win32.whl", hash = "sha256:59421806c1a0803ea7de9ed061d656c041a84db0da7e73266b98db4c7ba263da"}, - {file = "psycopg2_binary-2.9.8-cp311-cp311-win_amd64.whl", hash = "sha256:ccaa2ae03990cedde1f618ff11ec89fefa84622da73091a67b44553ca8be6711"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5aa0c99c12075c593dcdccbb8a7aaa714b716560cc99ef9206f9e75b77520801"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91719f53ed2a95ebecefac48d855d811cba9d9fe300acc162993bdfde9bc1c3b"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c68a2e1afb4f2a5bb4b7bb8f90298d21196ac1c66418523e549430b8c4b7cb1e"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278ebd63ced5a5f3af5394cb75a9a067243eee21f42f0126c6f1cf85eaeb90f9"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c84ff9682bc4520504c474e189b3de7c4a4029e529c8b775e39c95c33073767"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6f5e70e40dae47a4dc7f8eb390753bb599b0f4ede314580e6faa3b7383695d19"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:673eafbdaa4ed9f5164c90e191c3895cc5f866b9b379fdb59f3a2294e914d9bd"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5a0a6e4004697ec98035ff3b8dfc4dba8daa477b23ee891d831cd3cd65ace6be"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d29efab3c5d6d978115855a0f2643e0ee8c6450dc536d5b4afec6f52ab99e99e"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-win32.whl", hash = "sha256:d4a19a3332f2ac6d093e60a6f1c589f97eb9f9de7e27ea80d67f188384e31572"}, - {file = "psycopg2_binary-2.9.8-cp37-cp37m-win_amd64.whl", hash = "sha256:5262713988d97a9d4cd54b682dec4a413b87b76790e5b16f480450550d11a8f7"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e46b0f4683539965ce849f2c13fc53e323bb08d84d4ba2e4b3d976f364c84210"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3fd44b52bc9c74c1512662e8da113a1c55127adeeacebaf460babe766517b049"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b6c607ecb6a9c245ebe162d63ccd9222d38efa3c858bbe38d32810b08b8f87e"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6ef615d48fa60361e57f998327046bd89679c25d06eee9e78156be5a7a76e03"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65403113ac3a4813a1409fb6a1e43c658b459cc8ed8afcc5f4baf02ec8be4334"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debcb23a052f3fb4c165789ea513b562b2fac0f0f4f53eaf3cf4dc648907ff8"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dc145a241e1f6381efb924bcf3e3462d6020b8a147363f9111eb0a9c89331ad7"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1d669887df169a9b0c09e0f5b46891511850a9ddfcde3593408af9d9774c5c3a"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:19d40993701e39c49b50e75cd690a6af796d7e7210941ee0fe49cf12b25840e5"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b8b2cdf3bce4dd91dc035fbff4eb812f5607dda91364dc216b0920b97b521c7"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-win32.whl", hash = "sha256:4960c881471ca710b81a67ef148c33ee121c1f8e47a639cf7e06537fe9fee337"}, - {file = "psycopg2_binary-2.9.8-cp38-cp38-win_amd64.whl", hash = "sha256:aeb09db95f38e75ae04e947d283e07be34d03c4c2ace4f0b73dbb9143d506e67"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5aef3296d44d05805e634dbbd2972aa8eb7497926dd86047f5e39a79c3ecc086"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d6b592ecc8667e608b9e7344259fbfb428cc053df0062ec3ac75d8270cd5a9f"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:693a4e7641556f0b421a7d6c6a74058aead407d860ac1cb9d0bf25be0ca73de8"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf60c599c40c266a01c458e9c71db7132b11760f98f08233f19b3e0a2153cbf1"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cbe1e19f59950afd66764e3c905ecee9f2aee9f8df2ef35af6f7948ad93f620"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc37de7e3a87f5966965fc874d33c9b68d638e6c3718fdf32a5083de563428b0"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e1bb4eb0d9925d65dabaaabcbb279fab444ba66d73f86d4c07dfd11f0139c06"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e7bdc94217ae20ad03b375a991e107a31814053bee900ad8c967bf82ef3ff02e"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:565edaf9f691b17a7fdbabd368b5b3e67d0fdc8f7f6b52177c1d3289f4e763fd"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0e3071c947bda6afc6fe2e7b64ebd64fb2cad1bc0e705a3594cb499291f2dfec"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-win32.whl", hash = "sha256:205cecdd81ff4f1ddd687ce7d06879b9b80cccc428d8d6ebf36fcba08bb6d361"}, - {file = "psycopg2_binary-2.9.8-cp39-cp39-win_amd64.whl", hash = "sha256:1f279ba74f0d6b374526e5976c626d2ac3b8333b6a7b08755c513f4d380d3add"}, + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, ] [[package]] name = "pydantic" -version = "2.6.1" +version = "2.6.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, - {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.2" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -561,90 +605,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.2" +version = "2.16.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, - {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, - {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, - {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, - {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, - {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, - {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, - {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, - {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, - {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, - {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, - {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, - {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, - {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -652,13 +696,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -666,13 +710,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] @@ -680,38 +724,38 @@ cli = ["click (>=5.0)"] [[package]] name = "python-telegram-bot" -version = "20.5" +version = "21.0.1" description = "We have made you a wrapper you can't refuse" optional = false python-versions = ">=3.8" files = [ - {file = "python-telegram-bot-20.5.tar.gz", hash = "sha256:2f45a94c861cbd40440ece2be176ef0fc69e10d84e6dfa17f9a456e32aeece13"}, - {file = "python_telegram_bot-20.5-py3-none-any.whl", hash = "sha256:fc9605a855794231c802cc3948e6f7c319a817b5cd1827371f170bc7ca0ca279"}, + {file = "python-telegram-bot-21.0.1.tar.gz", hash = "sha256:3e005962c9fda01b09480044c49b3dd70870ee0c63340374bf3d5191e3910be9"}, + {file = "python_telegram_bot-21.0.1-py3-none-any.whl", hash = "sha256:b282544d1a51bf228b868e2ce0285b8448982878e2362175836429722d6f8795"}, ] [package.dependencies] -httpx = ">=0.24.1,<0.25.0" +httpx = ">=0.27,<1.0" [package.extras] -all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] -callback-data = ["cachetools (>=5.3.1,<5.4.0)"] -ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] +all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.3,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.4,<7.0)"] +callback-data = ["cachetools (>=5.3.3,<5.4.0)"] +ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.3,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.4,<7.0)"] http2 = ["httpx[http2]"] job-queue = ["APScheduler (>=3.10.4,<3.11.0)", "pytz (>=2018.6)"] passport = ["cryptography (>=39.0.1)"] rate-limiter = ["aiolimiter (>=1.1.0,<1.2.0)"] socks = ["httpx[socks]"] -webhooks = ["tornado (>=6.2,<7.0)"] +webhooks = ["tornado (>=6.4,<7.0)"] [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -737,13 +781,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "schedule" -version = "1.2.0" +version = "1.2.1" description = "Job scheduling for humans." optional = false python-versions = ">=3.7" files = [ - {file = "schedule-1.2.0-py2.py3-none-any.whl", hash = "sha256:415908febaba0bc9a7c727a32efb407d646fe994367ef9157d123aabbe539ea8"}, - {file = "schedule-1.2.0.tar.gz", hash = "sha256:b4ad697aafba7184c9eb6a1e2ebc41f781547242acde8ceae9a0a25b04c0922d"}, + {file = "schedule-1.2.1-py2.py3-none-any.whl", hash = "sha256:14cdeb083a596aa1de6dc77639a1b2ac8bf6eaafa82b1c9279d3612823063d01"}, + {file = "schedule-1.2.1.tar.gz", hash = "sha256:843bc0538b99c93f02b8b50e3e39886c06f2d003b24f48e1aa4cadfa3f341279"}, ] [[package]] @@ -759,72 +803,81 @@ files = [ [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlalchemy" -version = "2.0.21" +version = "2.0.28" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-win32.whl", hash = "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4"}, - {file = "SQLAlchemy-2.0.21-cp310-cp310-win_amd64.whl", hash = "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"}, - {file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-win32.whl", hash = "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9"}, - {file = "SQLAlchemy-2.0.21-cp37-cp37m-win_amd64.whl", hash = "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-win32.whl", hash = "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6"}, - {file = "SQLAlchemy-2.0.21-cp38-cp38-win_amd64.whl", hash = "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-win32.whl", hash = "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9"}, - {file = "SQLAlchemy-2.0.21-cp39-cp39-win_amd64.whl", hash = "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce"}, - {file = "SQLAlchemy-2.0.21-py3-none-any.whl", hash = "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce"}, - {file = "SQLAlchemy-2.0.21.tar.gz", hash = "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"}, + {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"}, + {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"}, ] [package.dependencies] greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -typing-extensions = ">=4.2.0" +typing-extensions = ">=4.6.0" [package.extras] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] @@ -834,7 +887,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)"] +oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -844,48 +897,48 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "tzdata" -version = "2023.3" +version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] name = "urllib3" -version = "2.0.4" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.11.5" -content-hash = "f4400d6f59c98014b1e5d667d4ccdf218a160b74280f14274ea673df20bc42e6" +content-hash = "eb23a24764bbd1ce75f96175e6afa78fdc57c4a890a9a7a75f805e27e5deb98e" diff --git a/pyproject.toml b/pyproject.toml index 84fb700..3d9cd0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ python-dotenv = "^1.0.0" numpy = "^1.23.2" pandas = "^2.1.0" schedule = "^1.2.0" -python-telegram-bot = "^20.5" +python-telegram-bot = "^21.0" sqlalchemy = "^2.0.21" psycopg2-binary = "^2.9.8" pydantic = "^2.6.1" diff --git a/src/alembic/versions/94c08ee5ba68_office_area_in_officeobject_changed_to_.py b/src/alembic/versions/94c08ee5ba68_office_area_in_officeobject_changed_to_.py new file mode 100644 index 0000000..c648d96 --- /dev/null +++ b/src/alembic/versions/94c08ee5ba68_office_area_in_officeobject_changed_to_.py @@ -0,0 +1,36 @@ +"""Office area in OfficeObject changed to float + +Revision ID: 94c08ee5ba68 +Revises: afa9f3aca061 +Create Date: 2024-03-21 05:37:08.838273 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '94c08ee5ba68' +down_revision: Union[str, None] = 'afa9f3aca061' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('officeobject', 'office_area', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('officeobject', 'office_area', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=True) + # ### end Alembic commands ### diff --git a/src/alembic/versions/afa9f3aca061_add_name_to_officeobject.py b/src/alembic/versions/afa9f3aca061_add_name_to_officeobject.py new file mode 100644 index 0000000..9b6b0e8 --- /dev/null +++ b/src/alembic/versions/afa9f3aca061_add_name_to_officeobject.py @@ -0,0 +1,30 @@ +"""Add name to OfficeObject + +Revision ID: afa9f3aca061 +Revises: 84153a08c3db +Create Date: 2024-03-06 07:54:20.582700 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'afa9f3aca061' +down_revision: Union[str, None] = '84153a08c3db' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('officeobject', sa.Column('name', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('officeobject', 'name') + # ### end Alembic commands ### diff --git a/src/convert_csv.py b/src/convert_csv.py new file mode 100644 index 0000000..d61f005 --- /dev/null +++ b/src/convert_csv.py @@ -0,0 +1,18 @@ +import csv + +input_filename = 'data.csv' +output_filename = 'processed_data.csv' + +with open(input_filename, mode='r', encoding='utf-8') as infile, open(output_filename, mode='w', encoding='utf-8', newline='') as outfile: + reader = csv.reader(infile) + writer = csv.writer(outfile, quoting=csv.QUOTE_MINIMAL) + + headers = next(reader) # Считывание заголовков + writer.writerow(headers) # Запись заголовков в выходной файл + + for row in reader: + # Оборачиваем значение в столбце `name` (индекс 1) в двойные кавычки + writer.writerow([row[0], f'"{row[1]}"', row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9]]) + + +print(f'Файл успешно обработан и сохранен как {output_filename}.') diff --git a/src/db/base_class.py b/src/db/base_class.py index 32c17e2..5f25cc4 100644 --- a/src/db/base_class.py +++ b/src/db/base_class.py @@ -28,3 +28,4 @@ def from_dto(cls, dto: BaseModel): Base = declarative_base(cls=CustomBase) + diff --git a/src/initial_data.py b/src/initial_data.py index 727f7b0..2674c69 100644 --- a/src/initial_data.py +++ b/src/initial_data.py @@ -7,37 +7,51 @@ # set up logging logger = WBLogger(__name__).get_logger() +with SessionLocal() as db: + with open('processed_data.csv', 'r', encoding='utf-8-sig') as file: + csv_reader = csv.reader(file) + headers = next(csv_reader) -with open('data.csv', 'r', encoding='utf-8-sig') as file: - csv_reader = csv.DictReader(file, fieldnames=['office_id', 'company', 'manager', 'office_area', 'rent', - 'salary_rate', 'min_wage', 'internet', 'administration'], - ) - next(csv_reader) - for row in csv_reader: - logger.info(f'row - {row}') - office_id = int(row.get('office_id', 0)) - company = row.get('company', '') - manager = row.get('manager', '') - office_area = int(row['office_area']) if row['office_area'] else 0 - rent = float(row.get('rent', 0.0)) - salary_rate = float(row.get('salary_rate', 0.0)) - min_wage = int(row.get('min_wage', 0)) - internet = int(row.get('internet', 0)) - administration = int(row.get('administration', 0)) + for row in csv_reader: + office_id = int(row[0]) + name = row[1].strip('"') + company = row[2] + manager = row[3] + office_area = int(row[4]) if row[4] else 0 + rent = float(row[5]) + salary_rate = float(row[6]) + min_wage = int(row[7]) + internet = int(row[8]) + administration = int(row[9]) + + existing_office = db.query(OfficeObject).filter_by(office_id=office_id).first() + if not existing_office: + logger.info('Initial new data to OfficeObject') + office_object = OfficeObject( + office_id=office_id, + name=name, + company=company, + manager=manager, + office_area=office_area, + rent=rent, + salary_rate=salary_rate, + min_wage=min_wage, + internet=internet, + administration=administration + ) + db.add(office_object) + logger.info(f'Office ID {office_object.office_id} with name {office_object.name} was added') + else: + logger.info('Trying to update office data...') + existing_office.name = name + existing_office.company = company + existing_office.manager = manager + existing_office.office_area = office_area + existing_office.rent = rent + existing_office.salary_rate = salary_rate + existing_office.min_wage = min_wage + existing_office.internet = internet + existing_office.administration = administration + + db.commit() - office_object = OfficeObject( - office_id=office_id, - company=company, - manager=manager, - office_area=office_area, - rent=rent, - salary_rate=salary_rate, - min_wage=min_wage, - internet=internet, - administration=administration - ) - db = SessionLocal() - db.merge(office_object) - logger.info(f'Office ID {office_object.office_id} was added') - db.commit() -logger.info('Applied initial office data') diff --git a/src/main_bot.py b/src/main_bot.py index 7aacd4d..ffb0d7f 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -1,33 +1,62 @@ -from telegram import Update +import itertools + +from warnings import filterwarnings + +from pydantic import ValidationError +from telegram.warnings import PTBUserWarning +from telegram.ext import CallbackQueryHandler + +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardRemove from telegram.ext import (CommandHandler, ContextTypes, ConversationHandler, - CallbackContext, ApplicationBuilder, + CallbackContext, ApplicationBuilder, CallbackQueryHandler, MessageHandler, filters) from bot.utility import validate_phone_number, validate_date +from db.db import get_db from parser.auth_api import AuthApi from parser.auth_fr import Auth from bot.logger import WBLogger from parser.api import ParserWB from parser.column_names import sale_data_column_names_mapping, \ operations_data_column_names_mapping -from utils.env import TELEGRAM_TOKEN +from parser.constants import field_names +from parser.schemas import OfficeModel +from utils.env import TELEGRAM_TOKEN, TELEGRAM_TOKEN2 from telegram import ReplyKeyboardMarkup from bot.utility import restricted import sys import os +from parser.service import get_all_offices, get_office_info, delete_office, update_office_field, add_office + sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) # set up logging logger = WBLogger(__name__).get_logger() +# remove warning +filterwarnings(action="ignore", message=r".*CallbackQueryHandler", category=PTBUserWarning) + GREET, GET_PHONE, GET_CODE, GET_START_DATE, GET_END_DATE = range(5) -SALES_REPORT, OPERATIONS_REPORT = range(6, 8) + + +OFFICES_MENU, VIEW_OFFICES, ADD_OFFICE, SELECT_OFFICE, EDIT_OFFICE, DELETE_OFFICE = range(5, 11) + +# Shortcut for ConversationHandler.END +END = ConversationHandler.END + +# Pagination +current_page = 0 +items_per_page = 10 + +# Constants for bot @restricted async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Send a message when the command /start is issued. + + :param update: Update :param context: Context :return: int""" @@ -40,6 +69,295 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: return GET_PHONE +@restricted +async def offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """ Команда для работы с офисами """ + + text = ( + "Вы можете просмотреть, изменить данные по офисам или удалить офис или завершить диалог" + "\nДля отмены просто введите команду /cancel." + ) + buttons = [ + [ + InlineKeyboardButton(text="Просмотреть офисы", callback_data=str(VIEW_OFFICES)), + InlineKeyboardButton(text="Добавить офис", callback_data=str(ADD_OFFICE)), + ] + ] + keyboard = InlineKeyboardMarkup(buttons) + + query = update.callback_query + if query: + # Если да, то редактируем сообщение + await query.edit_message_text(text=text, reply_markup=keyboard) + else: + # Если нет, то отправляем новое сообщение + await update.message.reply_text(text=text, reply_markup=keyboard) + + return OFFICES_MENU + + +# Просмотр офисов +async def view_offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + logger.info('View offices') + query = update.callback_query + if query: + await query.answer() + current_page = context.user_data.get('current_page', 0) + with get_db() as db: + office_list = get_all_offices(db_session=db) + total_offices = len(office_list) + pages = total_offices // items_per_page + (total_offices % items_per_page > 0) + offices = office_list[current_page * items_per_page:(current_page + 1) * items_per_page] + + if not offices: + text = "Список офисов пуст" + if query: + await query.answer(text, show_alert=True) + else: + await context.bot.send_message(chat_id=update.effective_chat.id, text=text) + return OFFICES_MENU + + keyboard_buttons = [] + for office in offices: + button_text = office['name'] + callback_data = f"office_{office['office_id']}" + keyboard_buttons.append([InlineKeyboardButton(text=button_text, callback_data=callback_data)]) + + # Добавляем кнопки пагинации в отдельный ряд + pagination_buttons = [] + if current_page > 0: + pagination_buttons.append(InlineKeyboardButton("Назад", callback_data="prev_page")) + if current_page < pages - 1: + pagination_buttons.append(InlineKeyboardButton("Вперед", callback_data="next_page")) + if pagination_buttons: + keyboard_buttons.append(pagination_buttons) + + keyboard_buttons.append([InlineKeyboardButton("В начальное меню", callback_data="return_to_start")]) + + reply_markup = InlineKeyboardMarkup(keyboard_buttons) + message_text = "Список офисов:" + if query: + await query.edit_message_text(text=message_text, reply_markup=reply_markup) + else: + await context.bot.send_message(chat_id=update.effective_chat.id, text=message_text, reply_markup=reply_markup) + + return VIEW_OFFICES + + +# Обработчики для кнопок пагинации + +async def previous_page(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + context.user_data['current_page'] = max(context.user_data.get('current_page', 0) - 1, 0) + await view_offices(update, context) + return VIEW_OFFICES + + +async def next_page(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + context.user_data['current_page'] = context.user_data.get('current_page', 0) + 1 + await view_offices(update, context) + return VIEW_OFFICES + + +async def return_to_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + context.user_data['current_page'] = 0 + await offices(update, context) + return OFFICES_MENU + + +async def edit_office_field(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + callback_data = query.data + _, field, office_id = callback_data.split('_') + # Сохраняем информацию о том, какое поле и офис нужно обновить, в context.user_data + context.user_data['edit_office'] = {'office_id': office_id, 'field': field} + # Запрашиваем у пользователя новое значение для поля + await context.bot.send_message(chat_id=update.effective_chat.id, + text=f"Введите новое значение для {field}:") + return EDIT_OFFICE + + +async def save_office_field(update: Update, context: ContextTypes.DEFAULT_TYPE): + text = update.message.text + office_id = context.user_data['edit_office']['office_id'] + field = context.user_data['edit_office']['field'] + + # Обновляем поле в базе данных + with get_db() as db: + success = update_office_field(db, office_id, field, text) + if success: + field_name = field_names.get(field, field) + await context.bot.send_message(chat_id=update.effective_chat.id, + text=f"Поле {field_name} было обновлено.") + else: + await context.bot.send_message(chat_id=update.effective_chat.id, + text="Произошла ошибка при обновлении поля.") + await select_office(update, context, office_id) + return SELECT_OFFICE + + +# Просмотр информации по офису +async def select_office(update: Update, context: ContextTypes.DEFAULT_TYPE, office_id=None): + query = update.callback_query + logger.info('Begin select office ') + if query: + callback_data = query.data + if not office_id and callback_data.startswith("office_"): + office_id = callback_data.split("_")[1] + await query.answer() + else: + if not office_id: + logger.error("No office_id provided to select_office function.") + return + with get_db() as db: + office_info = get_office_info(db, office_id) + + if office_info: + message_texts = [f"Информация по офису {office_id} - {office_info['name']}:\n"] + + for field, value in office_info.items(): + if field == 'id': + continue + field_name = field_names.get(field, field) + message_texts.append(f"{field_name}: {value}") + edit_buttons = [] + for field in office_info.keys(): + if field == 'id': + continue + button_text = f"Изменить {field_names.get(field, field)}" + edit_buttons.append(InlineKeyboardButton(button_text, callback_data=f"edit_{field}_{office_id}")) + + keyboard = [edit_buttons[i:i + 2] for i in range(0, len(edit_buttons), 2)] + keyboard.append([InlineKeyboardButton("Назад", callback_data=f"back_to_view_offices"), + InlineKeyboardButton("Удалить офис", callback_data=f"delete_{office_id}")]) + reply_markup = InlineKeyboardMarkup(keyboard) + await context.bot.send_message(chat_id=update.effective_chat.id, text='\n'.join(message_texts), + reply_markup=reply_markup) + + else: + message_text = ["Неизвестный запрос"] + await context.bot.send_message(chat_id=update.effective_chat.id, text=message_text) + return SELECT_OFFICE + + +# Удаление офиса +async def confirm_delete_office(update: Update, + context: ContextTypes.DEFAULT_TYPE): + """ Обработчик подтверждения удаления офиса""" + + query = update.callback_query + await query.answer() + logger.info('Confirm delete office called') + office_id = query.data.split('_')[1] + confirm_text = f"Вы уверены, что хотите удалить офис с ID {office_id}?" + keyboard = InlineKeyboardMarkup([ + [InlineKeyboardButton("Да, удалить", callback_data=f"perform_delete_{office_id}")], + [InlineKeyboardButton("Отмена", callback_data=f"cancel_delete_{office_id}")] + ]) + await query.edit_message_text(text=confirm_text, reply_markup=keyboard) + + return DELETE_OFFICE + + +async def perform_delete_office(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ Обработчик нажатия кнопки Да, удалить""" + + logger.info('Perform Delete Office') + query = update.callback_query + callback_data = update.callback_query.data + await query.answer() + with get_db() as db: + if callback_data.startswith("perform_delete_"): + office_id = callback_data.split("_")[2] + success = delete_office(db, office_id) + if success: + await context.bot.send_message(chat_id=update.effective_chat.id, text=f"Офис с ID {office_id} был удален.") + await view_offices(update, context) + return VIEW_OFFICES + else: + await context.bot.send_message(chat_id=update.effective_chat.id, + text=f"Произошла ошибка при удалении офиса с ID {office_id}.") + return SELECT_OFFICE + else: + message_text = ["Неизвестный запрос"] + await context.bot.send_message(chat_id=update.effective_chat.id, text=message_text) + return SELECT_OFFICE + + +async def cancel_delete_office(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + office_id = query.data.split('_')[2] + await select_office(update, context, office_id) + return SELECT_OFFICE + + +async def add_office_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик для добавления нового офиса""" + query = update.callback_query + await query.answer() + logger.info('ADD office handler begin') + instructions = ( + "Введите данные о новом офисе. Каждый параметр должен быть на новой строке.\n\n" + "Формат:\n" + "Office ID\n" + "Название ПВЗ\n" + "Компания\n" + "РФ\n" + "Площадь\n" + "Аренда\n" + "Ставка оплаты\n" + "Минимальная оплата\n" + "Интернет\n" + "Администрация\n" + ) + await context.bot.send_message(chat_id=update.effective_chat.id, text=instructions) + return ADD_OFFICE + + +async def save_new_office(update: Update, context: ContextTypes.DEFAULT_TYPE): + + logger.info('Save new office begin') + # Получаем данные от пользователя + office_data_text = update.message.text.strip() + office_data_lines = office_data_text.split('\n') + if len(office_data_lines) < 10: + await context.bot.send_message(chat_id=update.effective_chat.id, + text="Вы ввели недостаточно данных. Пожалуйста, введите информацию по каждому параметру на новой строке.") + return ADD_OFFICE + try: + # Создаем экземпляр модели OfficeModel из введенных данных + office_data = OfficeModel( + office_id=office_data_lines[0], + name=office_data_lines[1], + company=office_data_lines[2], + manager=office_data_lines[3], + office_area=office_data_lines[4], + rent=office_data_lines[5], + salary_rate=office_data_lines[6], + min_wage=office_data_lines[7], + internet=office_data_lines[8], + administration=office_data_lines[9] + ) + except ValidationError as e: + await context.bot.send_message(chat_id=update.effective_chat.id, + text=f"Ошибка в введенных данных: {e}") + return ADD_OFFICE + with get_db() as db: + success = add_office(db, office_data.dict()) + if success: + await context.bot.send_message(chat_id=update.effective_chat.id, + text="Новый офис успешно добавлен.") + else: + await context.bot.send_message(chat_id=update.effective_chat.id, + text="Произошла ошибка при добавлении офиса.") + await view_offices(update, context) + return VIEW_OFFICES + + + +################################# +### Работа с API WB async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """ Состояние для получения номера телефона @@ -88,7 +406,6 @@ async def get_code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: context.user_data['code'] = code phone = context.user_data['phone'] session = context.user_data['auth'].connect_with_code(phone, code) - logger.info(f'Session after connect with code - {session}') if session: context.user_data['session'] = session await update.message.reply_text("Вы успешно авторизованы!") @@ -247,12 +564,25 @@ async def choose_report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i return GET_START_DATE + + async def get_plugin_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: pass +async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Отмена и окончание диалога""" + + await update.message.reply_text( + "Вы отменили выбор, для повтора можете выбрать команду /start или /offices", reply_markup=ReplyKeyboardRemove() + ) + + return ConversationHandler.END + + def main() -> None: - application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() + application = ApplicationBuilder().token(TELEGRAM_TOKEN2).build() + logger.info('Main function') conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ @@ -260,13 +590,58 @@ def main() -> None: GET_CODE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_code)], GREET: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=choose_report)], GET_START_DATE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_start_date)], - GET_END_DATE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_end_date)] + GET_END_DATE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_end_date)], + }, - fallbacks=[CommandHandler("start", start)], - per_user=True - ) + fallbacks=[CommandHandler("cancel", cancel)], + allow_reentry=True, + per_user=True, + ) application.add_handler(conv_handler) + # Обработчик команды /offices + conv_handler_office = ConversationHandler( + entry_points=[CommandHandler("offices", offices)], + states={ + OFFICES_MENU: [ + CallbackQueryHandler(view_offices, pattern='^' + str(VIEW_OFFICES) + '$'), + CallbackQueryHandler(add_office_handler, pattern='^' + str(ADD_OFFICE) + '$') + ], + VIEW_OFFICES: [ + + CallbackQueryHandler(select_office, pattern='^office_'), + CallbackQueryHandler(previous_page, pattern='^prev_page$'), + CallbackQueryHandler(next_page, pattern='^next_page$'), + CallbackQueryHandler(return_to_start, pattern='^return_to_start$') + + ], + ADD_OFFICE: [ + MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=save_new_office), + ], + SELECT_OFFICE: [ + + CallbackQueryHandler(view_offices, pattern='^back_to_view_offices$'), + CallbackQueryHandler(confirm_delete_office, pattern='^delete_'), + CallbackQueryHandler(edit_office_field, pattern='^edit_'), + CallbackQueryHandler(return_to_start, pattern='^return_to_start$') + ], + EDIT_OFFICE: [ + MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=save_office_field), + ], + + DELETE_OFFICE: [ + CallbackQueryHandler(perform_delete_office, pattern='^perform_delete_'), + CallbackQueryHandler(cancel_delete_office, pattern='^cancel_delete_'), + ], + + }, + fallbacks=[CommandHandler("cancel", cancel)], + allow_reentry=True, + per_user=True, + + ) + + application.add_handler(conv_handler_office) application.run_polling(allowed_updates=Update.ALL_TYPES) diff --git a/src/parser/constants.py b/src/parser/constants.py new file mode 100644 index 0000000..0252f3e --- /dev/null +++ b/src/parser/constants.py @@ -0,0 +1,12 @@ +field_names = { + 'office_id': 'Office ID', + 'name': 'Название ПВЗ', + 'company': 'Компания', + 'manager': 'РФ', + 'office_area': 'Площадь', + 'rent': 'Аренда', + 'salary_rate': 'Ставка оплаты', + 'min_wage': 'Минимальная оплата', + 'internet': 'Интернет', + 'administration': 'Администрация' +} \ No newline at end of file diff --git a/src/parser/models.py b/src/parser/models.py index 347b273..aba9a29 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -7,9 +7,10 @@ class OfficeObject(Base): id = Column(Integer, primary_key=True, index=True) office_id = Column(Integer, unique=True) + name = Column(String) company = Column(String) manager = Column(String) - office_area = Column(Integer, default=0) + office_area = Column(Float, default=0.0) rent = Column(Float) salary_rate = Column(Float) min_wage = Column(Integer) diff --git a/src/parser/schemas.py b/src/parser/schemas.py index ecc3209..d3bb1cc 100644 --- a/src/parser/schemas.py +++ b/src/parser/schemas.py @@ -1,6 +1,6 @@ from datetime import datetime -from pydantic import BaseModel, Field +from pydantic import BaseModel,constr class SaleObjectIn(BaseModel): @@ -22,3 +22,17 @@ class SaleObjectIn(BaseModel): office_speed_sum: float +class OfficeModel(BaseModel): + office_id: constr(strip_whitespace=True, min_length=1) + name: constr(strip_whitespace=True, min_length=1) + company: constr(strip_whitespace=True, min_length=1) + manager: constr(strip_whitespace=True, min_length=1) + office_area: constr(strip_whitespace=True, min_length=1) + rent: constr(strip_whitespace=True, min_length=1) + salary_rate: constr(strip_whitespace=True, min_length=1) + min_wage: constr(strip_whitespace=True, min_length=1) + internet: constr(strip_whitespace=True, min_length=1) + administration: constr(strip_whitespace=True, min_length=1) + + + diff --git a/src/parser/service.py b/src/parser/service.py index 3bf442b..99fec8c 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -1,6 +1,8 @@ from typing import List from pydantic import ValidationError +from sqlalchemy import inspect +from sqlalchemy.exc import SQLAlchemyError from db.db import get_db @@ -10,6 +12,8 @@ from sqlalchemy.orm.exc import NoResultFound from bot.logger import WBLogger +import csv + logger = WBLogger(__name__).get_logger() @@ -43,11 +47,78 @@ def get_or_none(session: Session, model, **kwargs): return None -def get_office_info(session: Session, office_id): +def get_all_offices(db_session): + """ Функция для получения всех офисов""" + + try: + offices = db_session.query(OfficeObject).all() + office_list = [{'office_id': office.office_id, 'name': office.name} for office in offices] + return office_list + except Exception as e: + logger.error(f"Error in get_all_offices: {e}") + return [] + + +def get_office_info(db_session, office_id): """Получение офиса констант по office_id""" - office_data = session.query(OfficeObject).filter_by(office_id=office_id).first() - return office_data if office_data else None + try: + office_data = db_session.query(OfficeObject).filter_by(office_id=office_id).first() + if office_data: + return {c.key: getattr(office_data, c.key) + for c in inspect(office_data).mapper.column_attrs} + return None + except Exception as e: + logger.error(f"Error in get_office_info: {e}") + return [] + + +def update_office_field(db_session, office_id, field, new_value): + """Обновление полей офиса""" + try: + office_data = db_session.query(OfficeObject).filter_by(office_id=office_id).first() + if office_data: + setattr(office_data, field, new_value) + db_session.commit() + return True + else: + logger.error(f"Office with ID {office_id} not found.") + return False + except SQLAlchemyError as e: + db_session.rollback() + logger.error(f"Error in update_office_field: {e}") + return False + + +def add_office(db_session, office_data): + """Добавление нового офиса в базу данных""" + try: + new_office = OfficeObject(**office_data) + db_session.add(new_office) + db_session.commit() + return True + except SQLAlchemyError as e: + db_session.rollback() + logger.error(f"Error in add_office: {e}") + return False + + +def delete_office(db_session, office_id): + """Удаление офиса по ID""" + + try: + office_to_delete = db_session.query(OfficeObject).filter_by(office_id=office_id).first() + if office_to_delete: + db_session.delete(office_to_delete) + db_session.commit() + return True + else: + logger.error(f"Office with ID {office_id} not found.") + return False + except SQLAlchemyError as e: + db_session.rollback() + logger.error(f"Error in delete_office: {e}") + return False def safe_sale_object_to_db(sale_objects: List[dict]): @@ -103,3 +174,19 @@ def safe_sale_object_to_db(sale_objects: List[dict]): except ValidationError as e: logger.error(f"Ошибка валидации данных: {e}") continue + + +def read_csv_to_dict(file_name): + """Читаем существующий CSV-файл и возвращаем содержимое в виде списка словарей.""" + + data = [] + with open(file_name, 'r', newline='', encoding='utf-8') as file: + csv_reader = csv.DictReader(file) + for row in csv_reader: + data.append(row) + return data + + +def update_csv_with_db_data(csv_file_name='processed_data.csv'): + existing_data = read_csv_to_dict(csv_file_name) + existing_data_indexed = {int(item['office_id']): item for item in existing_data} diff --git a/src/utils/env.py b/src/utils/env.py index 4fe9ff4..0a64211 100644 --- a/src/utils/env.py +++ b/src/utils/env.py @@ -30,6 +30,7 @@ # Telegram TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN') +TELEGRAM_TOKEN2 = os.getenv('TELEGRAM_TOKEN2') # ADMINS ADMINS_STRING = os.getenv('ADMINS') From c7cbb94b86db3a559d5069b550a788bb07ebf1f9 Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 21 Mar 2024 16:51:20 +0500 Subject: [PATCH 19/28] Change API bot --- src/main_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main_bot.py b/src/main_bot.py index ffb0d7f..f26af3a 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -581,7 +581,7 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: def main() -> None: - application = ApplicationBuilder().token(TELEGRAM_TOKEN2).build() + application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() logger.info('Main function') conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], From eff8510b1b3b0755a5d6f4bd6eabf6c5f037c1f6 Mon Sep 17 00:00:00 2001 From: deymonster Date: Fri, 22 Mar 2024 17:04:11 +0500 Subject: [PATCH 20/28] Bugfix with update fields --- src/main_bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main_bot.py b/src/main_bot.py index f26af3a..1ba476c 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -168,10 +168,12 @@ async def edit_office_field(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() callback_data = query.data - _, field, office_id = callback_data.split('_') + _, *field_parts, office_id = callback_data.split('_') + field = '_'.join(field_parts) # Сохраняем информацию о том, какое поле и офис нужно обновить, в context.user_data context.user_data['edit_office'] = {'office_id': office_id, 'field': field} # Запрашиваем у пользователя новое значение для поля + field = field_names.get(field, field) await context.bot.send_message(chat_id=update.effective_chat.id, text=f"Введите новое значение для {field}:") return EDIT_OFFICE From 769466a351e3f02b24e5caf9d3283c95d6ad27aa Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 26 Jun 2024 12:08:56 +0500 Subject: [PATCH 21/28] Add new report Officerates and new Table in DB --- src/alembic/versions/84bc6682f410_.py | 41 ++ src/alembic/versions/e11ac7c90b48_.py | 30 ++ src/alembic/versions/efbd848c3310_.py | 44 ++ src/main_bot.py | 578 ++++++++++++++++++-------- src/parser/api.py | 85 +++- src/parser/column_names.py | 13 + src/parser/models.py | 16 +- src/parser/schemas.py | 15 +- src/parser/service.py | 155 +++++-- 9 files changed, 758 insertions(+), 219 deletions(-) create mode 100644 src/alembic/versions/84bc6682f410_.py create mode 100644 src/alembic/versions/e11ac7c90b48_.py create mode 100644 src/alembic/versions/efbd848c3310_.py diff --git a/src/alembic/versions/84bc6682f410_.py b/src/alembic/versions/84bc6682f410_.py new file mode 100644 index 0000000..d14a892 --- /dev/null +++ b/src/alembic/versions/84bc6682f410_.py @@ -0,0 +1,41 @@ +"""empty message + +Revision ID: 84bc6682f410 +Revises: 94c08ee5ba68 +Create Date: 2024-06-26 06:39:57.504035 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '84bc6682f410' +down_revision: Union[str, None] = '94c08ee5ba68' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('officeratingobject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('office_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(), nullable=True), + sa.Column('office_avg_hours', sa.Float(), nullable=True), + sa.Column('avg_hours_by_region', sa.Float(), nullable=True), + sa.Column('avg_rate', sa.Float(), nullable=True), + sa.Column('avg_region_rate', sa.Float(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_officeratingobject_id'), 'officeratingobject', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_officeratingobject_id'), table_name='officeratingobject') + op.drop_table('officeratingobject') + # ### end Alembic commands ### diff --git a/src/alembic/versions/e11ac7c90b48_.py b/src/alembic/versions/e11ac7c90b48_.py new file mode 100644 index 0000000..b4de246 --- /dev/null +++ b/src/alembic/versions/e11ac7c90b48_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: e11ac7c90b48 +Revises: efbd848c3310 +Create Date: 2024-06-26 07:02:47.852535 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e11ac7c90b48' +down_revision: Union[str, None] = 'efbd848c3310' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('ix_officeratingobject_office_id'), 'officeratingobject', ['office_id'], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_officeratingobject_office_id'), table_name='officeratingobject') + # ### end Alembic commands ### diff --git a/src/alembic/versions/efbd848c3310_.py b/src/alembic/versions/efbd848c3310_.py new file mode 100644 index 0000000..d17766f --- /dev/null +++ b/src/alembic/versions/efbd848c3310_.py @@ -0,0 +1,44 @@ +"""empty message + +Revision ID: efbd848c3310 +Revises: 84bc6682f410 +Create Date: 2024-06-26 06:55:04.518398 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'efbd848c3310' +down_revision: Union[str, None] = '84bc6682f410' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('officeratingobject', sa.Column('office_name', sa.String(), nullable=True)) + op.add_column('officeratingobject', sa.Column('avg_hours', sa.Float(), nullable=True)) + op.add_column('officeratingobject', sa.Column('inbox_count', sa.Integer(), nullable=True)) + op.add_column('officeratingobject', sa.Column('limit_delivery', sa.Integer(), nullable=True)) + op.add_column('officeratingobject', sa.Column('total_count', sa.Integer(), nullable=True)) + op.add_column('officeratingobject', sa.Column('workload', sa.Float(), nullable=True)) + op.drop_column('officeratingobject', 'office_avg_hours') + op.drop_column('officeratingobject', 'name') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('officeratingobject', sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('officeratingobject', sa.Column('office_avg_hours', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) + op.drop_column('officeratingobject', 'workload') + op.drop_column('officeratingobject', 'total_count') + op.drop_column('officeratingobject', 'limit_delivery') + op.drop_column('officeratingobject', 'inbox_count') + op.drop_column('officeratingobject', 'avg_hours') + op.drop_column('officeratingobject', 'office_name') + # ### end Alembic commands ### diff --git a/src/main_bot.py b/src/main_bot.py index 1ba476c..e656bf5 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -6,41 +6,69 @@ from telegram.warnings import PTBUserWarning from telegram.ext import CallbackQueryHandler -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardRemove -from telegram.ext import (CommandHandler, ContextTypes, - ConversationHandler, - CallbackContext, ApplicationBuilder, CallbackQueryHandler, - MessageHandler, filters) +from telegram import ( + Update, + InlineKeyboardButton, + InlineKeyboardMarkup, + ReplyKeyboardRemove, +) +from telegram.ext import ( + CommandHandler, + ContextTypes, + ConversationHandler, + CallbackContext, + ApplicationBuilder, + CallbackQueryHandler, + MessageHandler, + filters, +) from bot.utility import validate_phone_number, validate_date from db.db import get_db from parser.auth_api import AuthApi from parser.auth_fr import Auth from bot.logger import WBLogger -from parser.api import ParserWB -from parser.column_names import sale_data_column_names_mapping, \ - operations_data_column_names_mapping +from parser.api import ParserWB, OfficeRates +from parser.column_names import ( + sale_data_column_names_mapping, + operations_data_column_names_mapping, + office_rates_column_names_mapping, +) from parser.constants import field_names -from parser.schemas import OfficeModel +from parser.schemas import OfficeModel, OfficeRatesModel from utils.env import TELEGRAM_TOKEN, TELEGRAM_TOKEN2 from telegram import ReplyKeyboardMarkup from bot.utility import restricted import sys import os -from parser.service import get_all_offices, get_office_info, delete_office, update_office_field, add_office +from parser.service import ( + get_all_offices, + get_office_info, + delete_office, + update_office_field, + add_office, + create_or_update_ratings, +) -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) # set up logging logger = WBLogger(__name__).get_logger() # remove warning -filterwarnings(action="ignore", message=r".*CallbackQueryHandler", category=PTBUserWarning) +filterwarnings( + action="ignore", message=r".*CallbackQueryHandler", category=PTBUserWarning +) -GREET, GET_PHONE, GET_CODE, GET_START_DATE, GET_END_DATE = range(5) +GREET, GET_PHONE, GET_CODE, GET_START_DATE, GET_END_DATE, GET_OFFICE_RATINGS_REPORT = ( + range(6) +) -OFFICES_MENU, VIEW_OFFICES, ADD_OFFICE, SELECT_OFFICE, EDIT_OFFICE, DELETE_OFFICE = range(5, 11) +OFFICES_MENU, VIEW_OFFICES, ADD_OFFICE, SELECT_OFFICE, EDIT_OFFICE, DELETE_OFFICE = ( + range(7, 13) +) + # Shortcut for ConversationHandler.END END = ConversationHandler.END @@ -65,21 +93,25 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: await update.message.reply_text( f"Приветствую тебя {user.first_name} " f"для продолжения необходимо авторизоваться в ЛК. " - f"Введите номер телефона привязанный к ЛК WB") + f"Введите номер телефона привязанный к ЛК WB" + ) return GET_PHONE @restricted async def offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - """ Команда для работы с офисами """ + """Команда для работы с офисами""" + context.user_data.clear() text = ( "Вы можете просмотреть, изменить данные по офисам или удалить офис или завершить диалог" "\nДля отмены просто введите команду /cancel." ) buttons = [ [ - InlineKeyboardButton(text="Просмотреть офисы", callback_data=str(VIEW_OFFICES)), + InlineKeyboardButton( + text="Просмотреть офисы", callback_data=str(VIEW_OFFICES) + ), InlineKeyboardButton(text="Добавить офис", callback_data=str(ADD_OFFICE)), ] ] @@ -98,16 +130,18 @@ async def offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: # Просмотр офисов async def view_offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - logger.info('View offices') + logger.info("View offices") query = update.callback_query if query: await query.answer() - current_page = context.user_data.get('current_page', 0) + current_page = context.user_data.get("current_page", 0) with get_db() as db: office_list = get_all_offices(db_session=db) total_offices = len(office_list) pages = total_offices // items_per_page + (total_offices % items_per_page > 0) - offices = office_list[current_page * items_per_page:(current_page + 1) * items_per_page] + offices = office_list[ + current_page * items_per_page : (current_page + 1) * items_per_page + ] if not offices: text = "Список офисов пуст" @@ -119,47 +153,62 @@ async def view_offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in keyboard_buttons = [] for office in offices: - button_text = office['name'] + button_text = office["name"] callback_data = f"office_{office['office_id']}" - keyboard_buttons.append([InlineKeyboardButton(text=button_text, callback_data=callback_data)]) + keyboard_buttons.append( + [InlineKeyboardButton(text=button_text, callback_data=callback_data)] + ) # Добавляем кнопки пагинации в отдельный ряд pagination_buttons = [] if current_page > 0: - pagination_buttons.append(InlineKeyboardButton("Назад", callback_data="prev_page")) + pagination_buttons.append( + InlineKeyboardButton("Назад", callback_data="prev_page") + ) if current_page < pages - 1: - pagination_buttons.append(InlineKeyboardButton("Вперед", callback_data="next_page")) + pagination_buttons.append( + InlineKeyboardButton("Вперед", callback_data="next_page") + ) if pagination_buttons: keyboard_buttons.append(pagination_buttons) - keyboard_buttons.append([InlineKeyboardButton("В начальное меню", callback_data="return_to_start")]) + keyboard_buttons.append( + [InlineKeyboardButton("В начальное меню", callback_data="return_to_start")] + ) reply_markup = InlineKeyboardMarkup(keyboard_buttons) message_text = "Список офисов:" if query: await query.edit_message_text(text=message_text, reply_markup=reply_markup) else: - await context.bot.send_message(chat_id=update.effective_chat.id, text=message_text, reply_markup=reply_markup) + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=message_text, + reply_markup=reply_markup, + ) return VIEW_OFFICES # Обработчики для кнопок пагинации + async def previous_page(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - context.user_data['current_page'] = max(context.user_data.get('current_page', 0) - 1, 0) + context.user_data["current_page"] = max( + context.user_data.get("current_page", 0) - 1, 0 + ) await view_offices(update, context) return VIEW_OFFICES async def next_page(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - context.user_data['current_page'] = context.user_data.get('current_page', 0) + 1 + context.user_data["current_page"] = context.user_data.get("current_page", 0) + 1 await view_offices(update, context) return VIEW_OFFICES async def return_to_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - context.user_data['current_page'] = 0 + context.user_data["current_page"] = 0 await offices(update, context) return OFFICES_MENU @@ -168,40 +217,46 @@ async def edit_office_field(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() callback_data = query.data - _, *field_parts, office_id = callback_data.split('_') - field = '_'.join(field_parts) + _, *field_parts, office_id = callback_data.split("_") + field = "_".join(field_parts) # Сохраняем информацию о том, какое поле и офис нужно обновить, в context.user_data - context.user_data['edit_office'] = {'office_id': office_id, 'field': field} + context.user_data["edit_office"] = {"office_id": office_id, "field": field} # Запрашиваем у пользователя новое значение для поля field = field_names.get(field, field) - await context.bot.send_message(chat_id=update.effective_chat.id, - text=f"Введите новое значение для {field}:") + await context.bot.send_message( + chat_id=update.effective_chat.id, text=f"Введите новое значение для {field}:" + ) return EDIT_OFFICE async def save_office_field(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text - office_id = context.user_data['edit_office']['office_id'] - field = context.user_data['edit_office']['field'] + office_id = context.user_data["edit_office"]["office_id"] + field = context.user_data["edit_office"]["field"] # Обновляем поле в базе данных with get_db() as db: success = update_office_field(db, office_id, field, text) if success: field_name = field_names.get(field, field) - await context.bot.send_message(chat_id=update.effective_chat.id, - text=f"Поле {field_name} было обновлено.") + await context.bot.send_message( + chat_id=update.effective_chat.id, text=f"Поле {field_name} было обновлено." + ) else: - await context.bot.send_message(chat_id=update.effective_chat.id, - text="Произошла ошибка при обновлении поля.") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Произошла ошибка при обновлении поля.", + ) await select_office(update, context, office_id) return SELECT_OFFICE # Просмотр информации по офису -async def select_office(update: Update, context: ContextTypes.DEFAULT_TYPE, office_id=None): +async def select_office( + update: Update, context: ContextTypes.DEFAULT_TYPE, office_id=None +): query = update.callback_query - logger.info('Begin select office ') + logger.info("Begin select office ") if query: callback_data = query.data if not office_id and callback_data.startswith("office_"): @@ -215,56 +270,84 @@ async def select_office(update: Update, context: ContextTypes.DEFAULT_TYPE, offi office_info = get_office_info(db, office_id) if office_info: - message_texts = [f"Информация по офису {office_id} - {office_info['name']}:\n"] + message_texts = [ + f"Информация по офису {office_id} - {office_info['name']}:\n" + ] for field, value in office_info.items(): - if field == 'id': + if field == "id": continue field_name = field_names.get(field, field) message_texts.append(f"{field_name}: {value}") edit_buttons = [] for field in office_info.keys(): - if field == 'id': + if field == "id": continue button_text = f"Изменить {field_names.get(field, field)}" - edit_buttons.append(InlineKeyboardButton(button_text, callback_data=f"edit_{field}_{office_id}")) - - keyboard = [edit_buttons[i:i + 2] for i in range(0, len(edit_buttons), 2)] - keyboard.append([InlineKeyboardButton("Назад", callback_data=f"back_to_view_offices"), - InlineKeyboardButton("Удалить офис", callback_data=f"delete_{office_id}")]) + edit_buttons.append( + InlineKeyboardButton( + button_text, callback_data=f"edit_{field}_{office_id}" + ) + ) + + keyboard = [edit_buttons[i : i + 2] for i in range(0, len(edit_buttons), 2)] + keyboard.append( + [ + InlineKeyboardButton( + "Назад", callback_data=f"back_to_view_offices" + ), + InlineKeyboardButton( + "Удалить офис", callback_data=f"delete_{office_id}" + ), + ] + ) reply_markup = InlineKeyboardMarkup(keyboard) - await context.bot.send_message(chat_id=update.effective_chat.id, text='\n'.join(message_texts), - reply_markup=reply_markup) + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="\n".join(message_texts), + reply_markup=reply_markup, + ) else: message_text = ["Неизвестный запрос"] - await context.bot.send_message(chat_id=update.effective_chat.id, text=message_text) + await context.bot.send_message( + chat_id=update.effective_chat.id, text=message_text + ) return SELECT_OFFICE # Удаление офиса -async def confirm_delete_office(update: Update, - context: ContextTypes.DEFAULT_TYPE): - """ Обработчик подтверждения удаления офиса""" +async def confirm_delete_office(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик подтверждения удаления офиса""" query = update.callback_query await query.answer() - logger.info('Confirm delete office called') - office_id = query.data.split('_')[1] + logger.info("Confirm delete office called") + office_id = query.data.split("_")[1] confirm_text = f"Вы уверены, что хотите удалить офис с ID {office_id}?" - keyboard = InlineKeyboardMarkup([ - [InlineKeyboardButton("Да, удалить", callback_data=f"perform_delete_{office_id}")], - [InlineKeyboardButton("Отмена", callback_data=f"cancel_delete_{office_id}")] - ]) + keyboard = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Да, удалить", callback_data=f"perform_delete_{office_id}" + ) + ], + [ + InlineKeyboardButton( + "Отмена", callback_data=f"cancel_delete_{office_id}" + ) + ], + ] + ) await query.edit_message_text(text=confirm_text, reply_markup=keyboard) return DELETE_OFFICE async def perform_delete_office(update: Update, context: ContextTypes.DEFAULT_TYPE): - """ Обработчик нажатия кнопки Да, удалить""" + """Обработчик нажатия кнопки Да, удалить""" - logger.info('Perform Delete Office') + logger.info("Perform Delete Office") query = update.callback_query callback_data = update.callback_query.data await query.answer() @@ -273,23 +356,30 @@ async def perform_delete_office(update: Update, context: ContextTypes.DEFAULT_TY office_id = callback_data.split("_")[2] success = delete_office(db, office_id) if success: - await context.bot.send_message(chat_id=update.effective_chat.id, text=f"Офис с ID {office_id} был удален.") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Офис с ID {office_id} был удален.", + ) await view_offices(update, context) return VIEW_OFFICES else: - await context.bot.send_message(chat_id=update.effective_chat.id, - text=f"Произошла ошибка при удалении офиса с ID {office_id}.") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Произошла ошибка при удалении офиса с ID {office_id}.", + ) return SELECT_OFFICE else: message_text = ["Неизвестный запрос"] - await context.bot.send_message(chat_id=update.effective_chat.id, text=message_text) + await context.bot.send_message( + chat_id=update.effective_chat.id, text=message_text + ) return SELECT_OFFICE async def cancel_delete_office(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() - office_id = query.data.split('_')[2] + office_id = query.data.split("_")[2] await select_office(update, context, office_id) return SELECT_OFFICE @@ -298,7 +388,7 @@ async def add_office_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) """Обработчик для добавления нового офиса""" query = update.callback_query await query.answer() - logger.info('ADD office handler begin') + logger.info("ADD office handler begin") instructions = ( "Введите данные о новом офисе. Каждый параметр должен быть на новой строке.\n\n" "Формат:\n" @@ -314,18 +404,23 @@ async def add_office_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) "Администрация\n" ) await context.bot.send_message(chat_id=update.effective_chat.id, text=instructions) + logger.info("ADD office handler end") return ADD_OFFICE async def save_new_office(update: Update, context: ContextTypes.DEFAULT_TYPE): - logger.info('Save new office begin') + logger.info("Save new office begin") # Получаем данные от пользователя office_data_text = update.message.text.strip() - office_data_lines = office_data_text.split('\n') + logger.info(f"office_data_text - {office_data_text}") + office_data_lines = office_data_text.split("\n") + logger.info(f"office_data_lines - {office_data_lines}") if len(office_data_lines) < 10: - await context.bot.send_message(chat_id=update.effective_chat.id, - text="Вы ввели недостаточно данных. Пожалуйста, введите информацию по каждому параметру на новой строке.") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Вы ввели недостаточно данных. Пожалуйста, введите информацию по каждому параметру на новой строке.", + ) return ADD_OFFICE try: # Создаем экземпляр модели OfficeModel из введенных данных @@ -339,77 +434,82 @@ async def save_new_office(update: Update, context: ContextTypes.DEFAULT_TYPE): salary_rate=office_data_lines[6], min_wage=office_data_lines[7], internet=office_data_lines[8], - administration=office_data_lines[9] + administration=office_data_lines[9], ) except ValidationError as e: - await context.bot.send_message(chat_id=update.effective_chat.id, - text=f"Ошибка в введенных данных: {e}") + await context.bot.send_message( + chat_id=update.effective_chat.id, text=f"Ошибка в введенных данных: {e}" + ) return ADD_OFFICE with get_db() as db: success = add_office(db, office_data.dict()) if success: - await context.bot.send_message(chat_id=update.effective_chat.id, - text="Новый офис успешно добавлен.") + await context.bot.send_message( + chat_id=update.effective_chat.id, text="Новый офис успешно добавлен." + ) else: - await context.bot.send_message(chat_id=update.effective_chat.id, - text="Произошла ошибка при добавлении офиса.") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Произошла ошибка при добавлении офиса.", + ) await view_offices(update, context) return VIEW_OFFICES - ################################# ### Работа с API WB -async def get_phone(update: Update, - context: ContextTypes.DEFAULT_TYPE) -> int: - """ Состояние для получения номера телефона +async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Состояние для получения номера телефона для авторизации, проверка наличия токена, если токен валидный то получаем сессию для работы, если токена нет или просрочен то ожидаем от пользователя код из ЛК - """ + """ phone_number = update.message.text phone = validate_phone_number(phone_number) if phone_number == "Отмена": await update.message.reply_text( - "Выберите действие или начните заново командой /start") + "Выберите действие или начните заново командой /start" + ) return GREET if phone is None: - await update.message.reply_text( - "Неверный номер телефона. Попробуйте еще раз") + await update.message.reply_text("Неверный номер телефона. Попробуйте еще раз") return GET_PHONE - context.user_data['auth'] = Auth() + context.user_data["auth"] = Auth() context.user_data["phone"] = phone - session = context.user_data['auth'].get_franchise_session(phone) - auth_status = context.user_data['auth'].get_auth_status() + session = context.user_data["auth"].get_franchise_session(phone) + auth_status = context.user_data["auth"].get_auth_status() if auth_status == "NEED_CODE": await update.message.reply_text( - f"Отлично! Вы ввели номер {phone}, теперь введите код из ЛК") + f"Отлично! Вы ввели номер {phone}, теперь введите код из ЛК" + ) return GET_CODE elif auth_status == "ERROR": await update.message.reply_text( - "Слишком много запросов кода. Повторите запрос позднее") + "Слишком много запросов кода. Повторите запрос позднее" + ) return ConversationHandler.END else: await update.message.reply_text(f"Вы уже авторизованы с номером {phone}!") - context.user_data['session'] = session + context.user_data["session"] = session return await show_menu(update) async def get_code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - """ Состояние для получения кода из ЛК от пользователя, + """Состояние для получения кода из ЛК от пользователя, далее отправка телефона и кода для получения новой сессии""" code = update.message.text if code == "Отмена": await update.message.reply_text( - "Выберите действие или начните заново командой /start") + "Выберите действие или начните заново командой /start" + ) return GREET - context.user_data['code'] = code - phone = context.user_data['phone'] - session = context.user_data['auth'].connect_with_code(phone, code) + context.user_data["code"] = code + phone = context.user_data["phone"] + session = context.user_data["auth"].connect_with_code(phone, code) if session: - context.user_data['session'] = session + context.user_data["session"] = session await update.message.reply_text("Вы успешно авторизованы!") return await show_menu(update) else: @@ -424,45 +524,48 @@ async def get_code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: # return GET_START_DATE -async def get_start_date(update: Update, - context: ContextTypes.DEFAULT_TYPE) -> int: - """ Состояние для получения от пользователя даты начала отчета""" +async def get_start_date(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Состояние для получения от пользователя даты начала отчета""" start_date = update.message.text if start_date == "Отмена": await update.message.reply_text( - "Выберите действие или начните заново командой /start") + "Выберите действие или начните заново командой /start" + ) return GREET start_date = validate_date(start_date) if start_date is None: - await update.message.reply_text( - "Неверный формат даты. Попробуйте еще раз") + await update.message.reply_text("Неверный формат даты. Попробуйте еще раз") return GET_START_DATE - context.user_data['start_date'] = start_date - await update.message.reply_text(f"Отлично! Дата начала отчета: {start_date}. " - f"Теперь введите дату окончания отчета в формате YYYY-MM-DD)") + context.user_data["start_date"] = start_date + await update.message.reply_text( + f"Отлично! Дата начала отчета: {start_date}. " + f"Теперь введите дату окончания отчета в формате YYYY-MM-DD)" + ) return GET_END_DATE async def get_end_date(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - """ Состояние для получения даты окончания отчета """ + """Состояние для получения даты окончания отчета""" end_date = update.message.text if end_date == "Отмена": - await update.message.reply_text("Выберите действие или начните заново командой /start") + await update.message.reply_text( + "Выберите действие или начните заново командой /start" + ) return GREET end_date = validate_date(end_date) if end_date is None: await update.message.reply_text("Неверный формат даты. Попробуйте еще раз") return GET_END_DATE - context.user_data['end_date'] = end_date + context.user_data["end_date"] = end_date await update.message.reply_text("Отчет готовится. Пожалуйста, подождите") await send_report(update, context) return await show_menu(update) async def send_report(update: Update, context: CallbackContext): - """ Отправка отчета """ + """Отправка отчета""" logger.info("Begin to send report") - session = context.user_data['session'] + session = context.user_data["session"] logger.info(f"Session: {session}") parser = ParserWB(session) logger.info(" Begin to fetch offices") @@ -471,17 +574,22 @@ async def send_report(update: Update, context: CallbackContext): logger.info(" Begin to fetch employees") parser.fetch_employees() logger.info(" End of fetch employees") - date_from_str = context.user_data['start_date'].strftime('%Y-%m-%d') - logger.info(f'Type of date from - {type(date_from_str)}') - date_to_str = context.user_data['end_date'].strftime('%Y-%m-%d') - report_type = context.user_data.get('report_type') - filename = '' + date_from_str = context.user_data["start_date"].strftime("%Y-%m-%d") + logger.info(f"Type of date from - {type(date_from_str)}") + date_to_str = context.user_data["end_date"].strftime("%Y-%m-%d") + report_type = context.user_data.get("report_type") + file: bytes = b"" + filename = "" if report_type == "sales": logger.info("Begin to fetch sales data") data = parser.fetch_sales_data(date_from=date_from_str, date_to=date_to_str) logger.info(f"Get data - {data}") filename = f"sales_data/sales_data_{date_from_str} - {date_to_str} - {context.user_data['phone']}.csv" - file = parser.save_csv_memory(data=data, filename=filename, column_names_mapping=sale_data_column_names_mapping) + file = parser.save_csv_memory( + data=data, + filename=filename, + column_names_mapping=sale_data_column_names_mapping, + ) elif report_type == "shortages": logger.info("Begin to fetch shortages") @@ -492,42 +600,52 @@ async def send_report(update: Update, context: CallbackContext): elif report_type == "operations": logger.info("Begin to fetch operations data") - operations_data = parser.fetch_operations_data(date_from=date_from_str, - date_to=date_to_str) - filename = f"operations_data/operations_data_{date_from_str} " \ - f"- {date_to_str} - {context.user_data['phone']}.csv" - file = parser.save_csv_memory(data=operations_data, - filename=filename, - column_names_mapping=operations_data_column_names_mapping) + operations_data = parser.fetch_operations_data( + date_from=date_from_str, date_to=date_to_str + ) + filename = ( + f"operations_data/operations_data_{date_from_str} " + f"- {date_to_str} - {context.user_data['phone']}.csv" + ) + file = parser.save_csv_memory( + data=operations_data, + filename=filename, + column_names_mapping=operations_data_column_names_mapping, + ) # parser.safe_to_csv_operations(data=operations_data, filename=filename) elif report_type == "managers": - logger.info(f'Begin to fetch managers data') - managers_data = parser.fetch_employee_data(date_from=date_from_str, - date_to=date_to_str) + logger.info(f"Begin to fetch managers data") + managers_data = parser.fetch_employee_data( + date_from=date_from_str, date_to=date_to_str + ) api_parser = AuthApi() api_parser.get_token() for employee in managers_data: - phone_number = employee.get('phone') - logger.info(f'Phone number - {phone_number}') + phone_number = employee.get("phone") + logger.info(f"Phone number - {phone_number}") api_data = api_parser.get_employee_data(phone_number) - logger.info(f'API data - {api_data}') + logger.info(f"API data - {api_data}") if api_data: for day in api_data: # дата записи входа - api_date = day.get('date_created').split('T')[0] + api_date = day.get("date_created").split("T")[0] # получаем ШК офиса - api_barcode = day.get('barcode') - for operation in employee.get('operations'): + api_barcode = day.get("barcode") + for operation in employee.get("operations"): operation_date = operation.date if api_date == operation_date: operation.barcode = api_barcode - filename = f"operations_data/manager_operations_data_{date_from_str} " \ - f"- {date_to_str} - {context.user_data['phone']}.csv" + filename = ( + f"operations_data/manager_operations_data_{date_from_str} " + f"- {date_to_str} - {context.user_data['phone']}.csv" + ) parser.save_to_csv_mananagers_operations(data=managers_data, filename=filename) await update.message.reply_text("Отчет сформирован") # with open(filename, 'rb') as file: # await context.bot.send_document(chat_id=update.effective_chat.id, document=file, filename=filename) - await context.bot.send_document(chat_id=update.effective_chat.id, document=file, filename=filename) + await context.bot.send_document( + chat_id=update.effective_chat.id, document=file, filename=filename + ) await update.message.reply_text("Отчет отправлен") return GREET @@ -538,7 +656,8 @@ async def show_menu(update: Update) -> int: ["Отчет по недостачам"], ["Отчет по операциям"], ["Отчет по менеджерам"], - ["Отмена"] + ["Отчет по показателям офисов"], + ["Отмена"], ] reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) await update.message.reply_text("Выберите действие", reply_markup=reply_markup) @@ -548,24 +667,98 @@ async def show_menu(update: Update) -> int: async def choose_report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: user_choice = update.message.text if user_choice == "Отчет по продажам": - context.user_data['report_type'] = 'sales' + context.user_data["report_type"] = "sales" elif user_choice == "Отчет по недостачам": - context.user_data['report_type'] = 'shortages' + context.user_data["report_type"] = "shortages" elif user_choice == "Отчет по операциям": - context.user_data['report_type'] = 'operations' + context.user_data["report_type"] = "operations" elif user_choice == "Отчет по менеджерам": - context.user_data['report_type'] = 'managers' + context.user_data["report_type"] = "managers" + elif user_choice == "Отчет по показателям офисов": + await update.message.reply_text( + "Отчет по показателям офисов готовится. Пожалуйста, подождите." + ) + await send_office_ratings_report(update, context) + return GREET elif user_choice == "Отмена": - await update.message.reply_text("Выберите действие или начните заново командой /start") + await update.message.reply_text( + "Выберите действие или начните заново командой /start" + ) return GREET else: - await update.message.reply_text("Неизвестный выбор. Пожалуйста, попробуйте снова") + await update.message.reply_text( + "Неизвестный выбор. Пожалуйста, попробуйте снова" + ) return GREET await update.message.reply_text("Введите дату начала отчета (в формате YYYY-MM-DD)") return GET_START_DATE +async def send_office_ratings_report( + update: Update, context: ContextTypes.DEFAULT_TYPE +): + """Отчет по показателям офисов""" + logger.info("Begin to send office ratings report") + session = context.user_data["session"] + parser = ParserWB(session) + logger.info("Begin to fetch office ratings data") + parser.fetch_offices() + office_ids = parser.get_offices_ids() + parser.fetch_offices_rates(office_ids) + updated_office_rate_instances = [] + for office_rate in parser.offices_rates: + office_id = office_rate.office_id + office = next( + (office for office in parser.offices if office.id == office_id), None + ) + if office: + office_name = office.name + updated_office_rate_instance = OfficeRatesModel( + office_id=office_id, + office_name=office_name, + avg_rate=office_rate.avg_rate, + avg_region_rate=office_rate.avg_region_rate, + avg_hours=office_rate.avg_hours, + avg_hours_by_region=office_rate.avg_hours_by_region, + inbox_count=office_rate.inbox_count, + limit_delivery=office_rate.limit_delivery, + total_count=office_rate.total_count, + workload=office_rate.workload, + ) + updated_office_rate_instances.append(updated_office_rate_instance) + else: + logger.warning(f"Офис с ID {office_id} не найден") + filename = "Office_Ratings.csv" + file = parser.save_csv_memory( + data=updated_office_rate_instances, + filename=filename, + column_names_mapping=office_rates_column_names_mapping, + ) + + with get_db() as db: + create_or_update_ratings( + list_in=updated_office_rate_instances, + index_elements=["office_id"], + on_conflict_set={ + "avg_rate", + "avg_region_rate", + "avg_hours", + "avg_hours_by_region", + "inbox_count", + "limit_delivery", + "total_count", + "workload", + }, + db=db, + ) + await update.message.reply_text("Отчет сформирован") + await context.bot.send_document( + chat_id=update.effective_chat.id, document=file, filename=filename + ) + await update.message.reply_text("Отчет отправлен и записан в БД") + + return await show_menu(update) async def get_plugin_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -576,7 +769,8 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Отмена и окончание диалога""" await update.message.reply_text( - "Вы отменили выбор, для повтора можете выбрать команду /start или /offices", reply_markup=ReplyKeyboardRemove() + "Вы отменили выбор, для повтора можете выбрать команду /start или /offices", + reply_markup=ReplyKeyboardRemove(), ) return ConversationHandler.END @@ -584,21 +778,45 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: def main() -> None: application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() - logger.info('Main function') + logger.info("Main function") conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ - GET_PHONE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_phone)], - GET_CODE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_code)], - GREET: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=choose_report)], - GET_START_DATE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_start_date)], - GET_END_DATE: [MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=get_end_date)], - + GET_PHONE: [ + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=get_phone + ) + ], + GET_CODE: [ + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=get_code + ) + ], + GREET: [ + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=choose_report + ) + ], + GET_START_DATE: [ + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=get_start_date + ) + ], + GET_END_DATE: [ + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=get_end_date + ) + ], + GET_OFFICE_RATINGS_REPORT: [ + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, + callback=send_office_ratings_report, + ) + ], }, fallbacks=[CommandHandler("cancel", cancel)], allow_reentry=True, per_user=True, - ) application.add_handler(conv_handler) # Обработчик команды /offices @@ -606,41 +824,43 @@ def main() -> None: entry_points=[CommandHandler("offices", offices)], states={ OFFICES_MENU: [ - CallbackQueryHandler(view_offices, pattern='^' + str(VIEW_OFFICES) + '$'), - CallbackQueryHandler(add_office_handler, pattern='^' + str(ADD_OFFICE) + '$') - ], + CallbackQueryHandler( + view_offices, pattern="^" + str(VIEW_OFFICES) + "$" + ), + CallbackQueryHandler( + add_office_handler, pattern="^" + str(ADD_OFFICE) + "$" + ), + ], VIEW_OFFICES: [ - - CallbackQueryHandler(select_office, pattern='^office_'), - CallbackQueryHandler(previous_page, pattern='^prev_page$'), - CallbackQueryHandler(next_page, pattern='^next_page$'), - CallbackQueryHandler(return_to_start, pattern='^return_to_start$') - - ], + CallbackQueryHandler(select_office, pattern="^office_"), + CallbackQueryHandler(previous_page, pattern="^prev_page$"), + CallbackQueryHandler(next_page, pattern="^next_page$"), + CallbackQueryHandler(return_to_start, pattern="^return_to_start$"), + ], ADD_OFFICE: [ - MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=save_new_office), + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=save_new_office + ), ], SELECT_OFFICE: [ - - CallbackQueryHandler(view_offices, pattern='^back_to_view_offices$'), - CallbackQueryHandler(confirm_delete_office, pattern='^delete_'), - CallbackQueryHandler(edit_office_field, pattern='^edit_'), - CallbackQueryHandler(return_to_start, pattern='^return_to_start$') + CallbackQueryHandler(view_offices, pattern="^back_to_view_offices$"), + CallbackQueryHandler(confirm_delete_office, pattern="^delete_"), + CallbackQueryHandler(edit_office_field, pattern="^edit_"), + CallbackQueryHandler(return_to_start, pattern="^return_to_start$"), ], EDIT_OFFICE: [ - MessageHandler(filters=filters.TEXT & ~filters.COMMAND, callback=save_office_field), - ], - + MessageHandler( + filters=filters.TEXT & ~filters.COMMAND, callback=save_office_field + ), + ], DELETE_OFFICE: [ - CallbackQueryHandler(perform_delete_office, pattern='^perform_delete_'), - CallbackQueryHandler(cancel_delete_office, pattern='^cancel_delete_'), + CallbackQueryHandler(perform_delete_office, pattern="^perform_delete_"), + CallbackQueryHandler(cancel_delete_office, pattern="^cancel_delete_"), ], - }, fallbacks=[CommandHandler("cancel", cancel)], allow_reentry=True, per_user=True, - ) application.add_handler(conv_handler_office) diff --git a/src/parser/api.py b/src/parser/api.py index 3b5475d..031f04d 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -237,7 +237,10 @@ def to_dict(self): class Office: """Класс для хранения офисов""" - def __init__(self, id: int, name: str, office_shk: str): + def __init__(self, + id: int, + name: str, + office_shk: str): self.id = id self.name = name self.office_shk = office_shk @@ -283,6 +286,34 @@ def __init__(self, def to_dict(self): return self.__dict__ +class OfficeRates: + """Класс для хранения показателей офисов""" + + def __init__(self, + office_id: int, # id офиса + avg_rate: float, # рейтинг + avg_region_rate: float, # рейтинг региона + avg_hours: float, # время раскладки в часах + avg_hours_by_region: float, # время раскладки в часах по региону + inbox_count: int, # товаров в коробках + limit_delivery: int, # лимит доставок + total_count: int, # товаров на пвз + workload: float, # загруженность заказами + office_name: str = None, # наименование офиса + ): + self.office_id = office_id + self.avg_rate = avg_rate + self.avg_region_rate = avg_region_rate + self.avg_hours = avg_hours + self.avg_hours_by_region = avg_hours_by_region + self.inbox_count = inbox_count + self.limit_delivery = limit_delivery + self.total_count = total_count + self.workload = workload + self.office_name = office_name + + def to_dict(self): + return self.__dict__ class ParserWB: """Parser for WB API""" @@ -295,6 +326,7 @@ def __init__(self, session): self.session = session self.access_token = None self.offices = [] + self.offices_rates = [] self.employees = [] self.supplier_id = None self.headers = { @@ -341,6 +373,10 @@ def _get_supplier_id(self): except Exception as e: logger.error(e) + def get_offices_ids(self): + """Get list of office ids""" + return [office.id for office in self.offices] + def fetch_employees(self): """Get list of employees from wb api - Получение списка сотрудников и запись экземпляр класса""" url = f"{self.base_url_v1}/account" @@ -408,6 +444,9 @@ def fetch_shortages_data(self, date_from: str = None, date_to: str = None): if shortages_response := self._get_response_data_wb(url=shortages_url, prefix='shortages'): for data in (shortages_response.get('offices') or []): + if data.get('office_id') == 0: + logger.warning(f"Неизвестный офис {data.get('office_id')}") + continue # skip empty office # Преобразование JSON в объект OfficeShortage shortages_office_data = OfficeShortage.from_dict(data) # Применение фильтрации по датам, если они были переданы @@ -457,6 +496,50 @@ def fetch_offices(self): except Exception as e: logger.error(e) + def fetch_offices_rates(self, office_ids: List[int]) -> None: + """Fetch rates of all offices""" + url_rates = f"{self.base_url_v1}/office/rates" + params = {'office_ids': ','.join(map(str, office_ids))} + url_speed = f"{self.base_url_v1}/office/on-place" + url_workload = f"{self.base_url_v1}/office/info/workload" + merged_data = {} + try: + if response := self._get_response_data_wb(url=url_rates, params=params, prefix='office_rates'): + logger.info('Office rates received successfully') + office_rates = response + # Обработка списка рейтингов + for item in office_rates: + office_id = item['office_id'] + if office_id not in merged_data: + merged_data[office_id] = {} + merged_data[office_id].update(item) + if response := self._get_response_data_wb(url=url_speed, params=params, prefix='office_on_place'): + logger.info('Offices speed received successfully') + offices_speed = response + # Обработка списка скорости офисов + for item in offices_speed: + office_id = item['office_id'] + if office_id not in merged_data: + merged_data[office_id] = {} + merged_data[office_id].update(item) + if response := self._get_response_data_wb(url=url_workload, params=params, prefix='office_info'): + logger.info('Offices workload received successfully') + offices_workload = response + # Обработка списка производительности + for item in offices_workload: + office_id = item['office_id'] + if office_id not in merged_data: + merged_data[office_id] = {} + merged_data[office_id].update(item) + + for office_id, data in merged_data.items(): + self.offices_rates.append(OfficeRates(**data)) + + except Exception as e: + logger.error(e) + + + def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ list[Any], list[SaleData]]: """Get sales data from wb api - Получение данных по продажам """ diff --git a/src/parser/column_names.py b/src/parser/column_names.py index 27a0b36..355510c 100644 --- a/src/parser/column_names.py +++ b/src/parser/column_names.py @@ -18,6 +18,19 @@ } +office_rates_column_names_mapping = { + 'office_id': 'ID офиса', + 'office_name': 'Название офиса', + 'avg_hours': 'Время раскладки', + 'avg_hours_by_region': 'Среднее время раскладки по региону', + 'avg_rate': 'Рейтинг филиала', + 'avg_rate_by_region': 'Средний рейтинг по региону', + 'inbox_count': 'Товаров в коробках', + 'limit_delivery': 'Лимит доставок', + 'total_count': 'Товаров на ПВЗ', + 'workload': 'Загруженность товарами' +} + operations_data_column_names_mapping = { 'date': 'Дата', diff --git a/src/parser/models.py b/src/parser/models.py index aba9a29..3b67db6 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -52,7 +52,17 @@ class SaleObject(Base): profitability = Column(Float) +class OfficeRatingObject(Base): + """Office ratings""" - - - + id = Column(Integer, primary_key=True, index=True) + office_id = Column(Integer, unique=True, index=True) + office_name = Column(String) + avg_hours = Column(Float) + avg_hours_by_region = Column(Float) + avg_rate = Column(Float) + avg_region_rate = Column(Float) + inbox_count = Column(Integer) + limit_delivery = Column(Integer) + total_count = Column(Integer) + workload = Column(Float) diff --git a/src/parser/schemas.py b/src/parser/schemas.py index d3bb1cc..e2d900f 100644 --- a/src/parser/schemas.py +++ b/src/parser/schemas.py @@ -1,10 +1,11 @@ from datetime import datetime -from pydantic import BaseModel,constr +from pydantic import BaseModel, constr, Field class SaleObjectIn(BaseModel): """Schema for SaleObject in DB""" + office_id: int name: str date: datetime @@ -35,4 +36,16 @@ class OfficeModel(BaseModel): administration: constr(strip_whitespace=True, min_length=1) +class OfficeRatesModel(BaseModel): + """Model for OfficeRates in DB""" + office_id: int + office_name: str | None + avg_hours: float | None + avg_hours_by_region: float | None + avg_rate: float | None + avg_region_rate: float | None + inbox_count: int | None + limit_delivery: int | None + total_count: int | None + workload: int | None diff --git a/src/parser/service.py b/src/parser/service.py index 99fec8c..005d5e4 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -1,13 +1,15 @@ -from typing import List +from typing import List, Set from pydantic import ValidationError -from sqlalchemy import inspect +from sqlalchemy import inspect, exc from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.dialects.postgresql import insert as pg_insert + from db.db import get_db -from parser.schemas import SaleObjectIn -from parser.models import SaleObject, OfficeObject +from parser.schemas import SaleObjectIn, OfficeRatesModel +from parser.models import SaleObject, OfficeObject, OfficeRatingObject from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound from bot.logger import WBLogger @@ -17,9 +19,23 @@ logger = WBLogger(__name__).get_logger() -def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, return_count, sale_sum, return_sum, - proceeds, amount, bags_sum, office_rating, percent, office_rating_sum, - supplier_return_sum, office_speed_sum): +def convert_sale_data_to_sale_object_in( + office_id, + date, + name, + sale_count, + return_count, + sale_sum, + return_sum, + proceeds, + amount, + bags_sum, + office_rating, + percent, + office_rating_sum, + supplier_return_sum, + office_speed_sum, +): return SaleObjectIn( office_id=office_id, date=date, @@ -35,12 +51,12 @@ def convert_sale_data_to_sale_object_in(office_id, date, name, sale_count, retur percent=round(float(percent), 2), office_rating_sum=int(office_rating_sum), supplier_return_sum=supplier_return_sum, - office_speed_sum=round(float(office_speed_sum), 2) + office_speed_sum=round(float(office_speed_sum), 2), ) def get_or_none(session: Session, model, **kwargs): - """ Функция для проверки наличие данных в БД""" + """Функция для проверки наличие данных в БД""" try: return session.query(model).filter_by(**kwargs).first() except NoResultFound: @@ -48,11 +64,13 @@ def get_or_none(session: Session, model, **kwargs): def get_all_offices(db_session): - """ Функция для получения всех офисов""" + """Функция для получения всех офисов""" try: offices = db_session.query(OfficeObject).all() - office_list = [{'office_id': office.office_id, 'name': office.name} for office in offices] + office_list = [ + {"office_id": office.office_id, "name": office.name} for office in offices + ] return office_list except Exception as e: logger.error(f"Error in get_all_offices: {e}") @@ -63,10 +81,14 @@ def get_office_info(db_session, office_id): """Получение офиса констант по office_id""" try: - office_data = db_session.query(OfficeObject).filter_by(office_id=office_id).first() + office_data = ( + db_session.query(OfficeObject).filter_by(office_id=office_id).first() + ) if office_data: - return {c.key: getattr(office_data, c.key) - for c in inspect(office_data).mapper.column_attrs} + return { + c.key: getattr(office_data, c.key) + for c in inspect(office_data).mapper.column_attrs + } return None except Exception as e: logger.error(f"Error in get_office_info: {e}") @@ -76,7 +98,9 @@ def get_office_info(db_session, office_id): def update_office_field(db_session, office_id, field, new_value): """Обновление полей офиса""" try: - office_data = db_session.query(OfficeObject).filter_by(office_id=office_id).first() + office_data = ( + db_session.query(OfficeObject).filter_by(office_id=office_id).first() + ) if office_data: setattr(office_data, field, new_value) db_session.commit() @@ -107,7 +131,9 @@ def delete_office(db_session, office_id): """Удаление офиса по ID""" try: - office_to_delete = db_session.query(OfficeObject).filter_by(office_id=office_id).first() + office_to_delete = ( + db_session.query(OfficeObject).filter_by(office_id=office_id).first() + ) if office_to_delete: db_session.delete(office_to_delete) db_session.commit() @@ -122,38 +148,65 @@ def delete_office(db_session, office_id): def safe_sale_object_to_db(sale_objects: List[dict]): - """ Сервис функция для валидации входных данных - списка SaleObjectIn и запись их в БД""" + """Сервис функция для валидации входных данных - списка SaleObjectIn и запись их в БД""" for sale_object in sale_objects: with get_db() as db: try: - logger.info(f'sale_object is - {sale_object}') - sale_object_in = convert_sale_data_to_sale_object_in(**sale_object.to_dict()) - existing_data = get_or_none(db, SaleObject, date=sale_object_in.date, - office_id=sale_object_in.office_id) + logger.info(f"sale_object is - {sale_object}") + sale_object_in = convert_sale_data_to_sale_object_in( + **sale_object.to_dict() + ) + existing_data = get_or_none( + db, + SaleObject, + date=sale_object_in.date, + office_id=sale_object_in.office_id, + ) if existing_data: logger.info( f"Данные для даты {sale_object_in.date} " - f"и офиса {sale_object_in.office_id} уже существуют. Пропускаем.") + f"и офиса {sale_object_in.office_id} уже существуют. Пропускаем." + ) continue # office_data = get_office_info(db, sale_object_in.office_id) - office_data = get_or_none(db, OfficeObject, office_id=sale_object_in.office_id) + office_data = get_or_none( + db, OfficeObject, office_id=sale_object_in.office_id + ) if not office_data: - logger.warning(f"Отсутствуют данные для офиса {sale_object_in.office_id}. Пропускаем.") + logger.warning( + f"Отсутствуют данные для офиса {sale_object_in.office_id}. Пропускаем." + ) continue - logger.info(f'Office data - {office_data}') + logger.info(f"Office data - {office_data}") # Вычисление дополнительных полей - reward_plan = round(float(sale_object_in.proceeds * sale_object_in.percent / 100), 2) - salary_fund_plan = round(float(sale_object_in.proceeds * office_data.salary_rate / 100), 2) - actual_salary_fund = round(float(max(salary_fund_plan, office_data.min_wage)), 2) - difference_salary_fund = round(float(actual_salary_fund - salary_fund_plan), 2), + reward_plan = round( + float(sale_object_in.proceeds * sale_object_in.percent / 100), 2 + ) + salary_fund_plan = round( + float(sale_object_in.proceeds * office_data.salary_rate / 100), 2 + ) + actual_salary_fund = round( + float(max(salary_fund_plan, office_data.min_wage)), 2 + ) + difference_salary_fund = ( + round(float(actual_salary_fund - salary_fund_plan), 2), + ) daily_rent = round(float(office_data.rent / 30), 2) daily_administration = round(float(office_data.administration / 30), 2) daily_internet = round(float(office_data.internet / 30), 2) maintenance = round(float(sale_object_in.proceeds / 1000000 * 630), 2) profitability = round( - float(reward_plan - actual_salary_fund - - daily_administration - daily_internet - maintenance - daily_rent), 2) + float( + reward_plan + - actual_salary_fund + - daily_administration + - daily_internet + - maintenance + - daily_rent + ), + 2, + ) db_data = SaleObject( **sale_object_in.model_dump(), @@ -167,7 +220,7 @@ def safe_sale_object_to_db(sale_objects: List[dict]): administration=daily_administration, internet=daily_internet, maintenance=maintenance, - profitability=profitability + profitability=profitability, ) db.add(db_data) logger.info(f"Данные записаны - {db_data}") @@ -180,13 +233,45 @@ def read_csv_to_dict(file_name): """Читаем существующий CSV-файл и возвращаем содержимое в виде списка словарей.""" data = [] - with open(file_name, 'r', newline='', encoding='utf-8') as file: + with open(file_name, "r", newline="", encoding="utf-8") as file: csv_reader = csv.DictReader(file) for row in csv_reader: data.append(row) return data -def update_csv_with_db_data(csv_file_name='processed_data.csv'): +def update_csv_with_db_data(csv_file_name="processed_data.csv"): existing_data = read_csv_to_dict(csv_file_name) - existing_data_indexed = {int(item['office_id']): item for item in existing_data} + existing_data_indexed = {int(item["office_id"]): item for item in existing_data} + + +def create_or_update_ratings( + list_in: List[OfficeRatesModel], + index_elements: List[str], + on_conflict_set: Set[str], + db: Session, +): + """Добавление рейтинга офиса в базу данных + + :param list_in: Список объектов + :param index_elements: Индексы элементов для проверки уникальности + :param on_conflict_set: Список полей, которые будут обновляться при конфликтах + :param db: База данных + """ + objs = [] + for obj_in in list_in: + obj_dict = obj_in.dict(exclude_unset=True) + objs.append(obj_dict) + + insert_query = pg_insert(OfficeRatingObject).values(objs) + insert_query = insert_query.on_conflict_do_update( + index_elements=index_elements, + set_={key: getattr(insert_query.excluded, key) for key in on_conflict_set}, + ).returning(OfficeRatingObject) + + try: + db.execute(insert_query) + db.commit() + except exc.IntegrityError as e: + db.rollback() + print(e) From 0fd99ec5a115252ecdc6b756ade1c872c495fe6f Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 26 Jun 2024 15:13:35 +0500 Subject: [PATCH 22/28] New order in officeratingobject --- src/alembic/versions/e11ac7c90b48_.py | 30 ------------- src/alembic/versions/efbd848c3310_.py | 44 ------------------- ...e703264cc_add_table_officeratingobject.py} | 19 +++++--- src/main_bot.py | 2 +- src/parser/models.py | 2 + 5 files changed, 16 insertions(+), 81 deletions(-) delete mode 100644 src/alembic/versions/e11ac7c90b48_.py delete mode 100644 src/alembic/versions/efbd848c3310_.py rename src/alembic/versions/{84bc6682f410_.py => f1ee703264cc_add_table_officeratingobject.py} (61%) diff --git a/src/alembic/versions/e11ac7c90b48_.py b/src/alembic/versions/e11ac7c90b48_.py deleted file mode 100644 index b4de246..0000000 --- a/src/alembic/versions/e11ac7c90b48_.py +++ /dev/null @@ -1,30 +0,0 @@ -"""empty message - -Revision ID: e11ac7c90b48 -Revises: efbd848c3310 -Create Date: 2024-06-26 07:02:47.852535 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'e11ac7c90b48' -down_revision: Union[str, None] = 'efbd848c3310' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_index(op.f('ix_officeratingobject_office_id'), 'officeratingobject', ['office_id'], unique=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_officeratingobject_office_id'), table_name='officeratingobject') - # ### end Alembic commands ### diff --git a/src/alembic/versions/efbd848c3310_.py b/src/alembic/versions/efbd848c3310_.py deleted file mode 100644 index d17766f..0000000 --- a/src/alembic/versions/efbd848c3310_.py +++ /dev/null @@ -1,44 +0,0 @@ -"""empty message - -Revision ID: efbd848c3310 -Revises: 84bc6682f410 -Create Date: 2024-06-26 06:55:04.518398 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'efbd848c3310' -down_revision: Union[str, None] = '84bc6682f410' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('officeratingobject', sa.Column('office_name', sa.String(), nullable=True)) - op.add_column('officeratingobject', sa.Column('avg_hours', sa.Float(), nullable=True)) - op.add_column('officeratingobject', sa.Column('inbox_count', sa.Integer(), nullable=True)) - op.add_column('officeratingobject', sa.Column('limit_delivery', sa.Integer(), nullable=True)) - op.add_column('officeratingobject', sa.Column('total_count', sa.Integer(), nullable=True)) - op.add_column('officeratingobject', sa.Column('workload', sa.Float(), nullable=True)) - op.drop_column('officeratingobject', 'office_avg_hours') - op.drop_column('officeratingobject', 'name') - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('officeratingobject', sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('officeratingobject', sa.Column('office_avg_hours', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) - op.drop_column('officeratingobject', 'workload') - op.drop_column('officeratingobject', 'total_count') - op.drop_column('officeratingobject', 'limit_delivery') - op.drop_column('officeratingobject', 'inbox_count') - op.drop_column('officeratingobject', 'avg_hours') - op.drop_column('officeratingobject', 'office_name') - # ### end Alembic commands ### diff --git a/src/alembic/versions/84bc6682f410_.py b/src/alembic/versions/f1ee703264cc_add_table_officeratingobject.py similarity index 61% rename from src/alembic/versions/84bc6682f410_.py rename to src/alembic/versions/f1ee703264cc_add_table_officeratingobject.py index d14a892..0ce84b6 100644 --- a/src/alembic/versions/84bc6682f410_.py +++ b/src/alembic/versions/f1ee703264cc_add_table_officeratingobject.py @@ -1,8 +1,8 @@ -"""empty message +"""Add table OfficeRatingObject -Revision ID: 84bc6682f410 +Revision ID: f1ee703264cc Revises: 94c08ee5ba68 -Create Date: 2024-06-26 06:39:57.504035 +Create Date: 2024-06-26 09:33:15.780766 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = '84bc6682f410' +revision: str = 'f1ee703264cc' down_revision: Union[str, None] = '94c08ee5ba68' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -23,19 +23,26 @@ def upgrade() -> None: op.create_table('officeratingobject', sa.Column('id', sa.Integer(), nullable=False), sa.Column('office_id', sa.Integer(), nullable=True), - sa.Column('name', sa.String(), nullable=True), - sa.Column('office_avg_hours', sa.Float(), nullable=True), + sa.Column('office_name', sa.String(), nullable=True), + sa.Column('avg_hours', sa.Float(), nullable=True), sa.Column('avg_hours_by_region', sa.Float(), nullable=True), sa.Column('avg_rate', sa.Float(), nullable=True), sa.Column('avg_region_rate', sa.Float(), nullable=True), + sa.Column('inbox_count', sa.Integer(), nullable=True), + sa.Column('limit_delivery', sa.Integer(), nullable=True), + sa.Column('total_count', sa.Integer(), nullable=True), + sa.Column('workload', sa.Float(), nullable=True), + sa.Column('created_at', sa.Date(), nullable=True), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_officeratingobject_id'), 'officeratingobject', ['id'], unique=False) + op.create_index(op.f('ix_officeratingobject_office_id'), 'officeratingobject', ['office_id'], unique=True) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_officeratingobject_office_id'), table_name='officeratingobject') op.drop_index(op.f('ix_officeratingobject_id'), table_name='officeratingobject') op.drop_table('officeratingobject') # ### end Alembic commands ### diff --git a/src/main_bot.py b/src/main_bot.py index e656bf5..407911f 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -777,7 +777,7 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: def main() -> None: - application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() + application = ApplicationBuilder().token(TELEGRAM_TOKEN2).build() logger.info("Main function") conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], diff --git a/src/parser/models.py b/src/parser/models.py index 3b67db6..865962e 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -1,5 +1,6 @@ from db.base_class import Base from sqlalchemy import Column, Integer, String, Date, DateTime, Float +from sqlalchemy.sql import func class OfficeObject(Base): @@ -66,3 +67,4 @@ class OfficeRatingObject(Base): limit_delivery = Column(Integer) total_count = Column(Integer) workload = Column(Float) + created_at = Column(Date, default=func.date(func.now())) From 07922fd743c37c910a0781c36928b78af651d0f9 Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 26 Jun 2024 15:22:05 +0500 Subject: [PATCH 23/28] Change token --- src/main_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main_bot.py b/src/main_bot.py index 407911f..e656bf5 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -777,7 +777,7 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: def main() -> None: - application = ApplicationBuilder().token(TELEGRAM_TOKEN2).build() + application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() logger.info("Main function") conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], From 70d2220ee1dd33769bf06112044514b6475fa94b Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 26 Jun 2024 21:50:21 +0500 Subject: [PATCH 24/28] Update logic while adding new OfficeRatingObject --- ...28_make_created_at_index_and_office_id_.py | 32 +++++++++++++++++++ src/main_bot.py | 2 +- src/parser/models.py | 8 +++-- src/parser/service.py | 5 ++- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/alembic/versions/446dbb27c428_make_created_at_index_and_office_id_.py diff --git a/src/alembic/versions/446dbb27c428_make_created_at_index_and_office_id_.py b/src/alembic/versions/446dbb27c428_make_created_at_index_and_office_id_.py new file mode 100644 index 0000000..7f6f79e --- /dev/null +++ b/src/alembic/versions/446dbb27c428_make_created_at_index_and_office_id_.py @@ -0,0 +1,32 @@ +"""make created_at index and office_id index together + +Revision ID: 446dbb27c428 +Revises: f1ee703264cc +Create Date: 2024-06-26 13:02:52.614962 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '446dbb27c428' +down_revision: Union[str, None] = 'f1ee703264cc' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('ix_officeratingobject_created_at'), 'officeratingobject', ['created_at'], unique=False) + op.create_unique_constraint('uq_office_rating', 'officeratingobject', ['office_id', 'created_at']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uq_office_rating', 'officeratingobject', type_='unique') + op.drop_index(op.f('ix_officeratingobject_created_at'), table_name='officeratingobject') + # ### end Alembic commands ### diff --git a/src/main_bot.py b/src/main_bot.py index e656bf5..b6bc7ac 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -739,7 +739,7 @@ async def send_office_ratings_report( with get_db() as db: create_or_update_ratings( list_in=updated_office_rate_instances, - index_elements=["office_id"], + index_elements=["office_id", "created_at"], on_conflict_set={ "avg_rate", "avg_region_rate", diff --git a/src/parser/models.py b/src/parser/models.py index 865962e..f343af0 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -1,5 +1,5 @@ from db.base_class import Base -from sqlalchemy import Column, Integer, String, Date, DateTime, Float +from sqlalchemy import Column, Integer, String, Date, DateTime, Float, UniqueConstraint from sqlalchemy.sql import func @@ -67,4 +67,8 @@ class OfficeRatingObject(Base): limit_delivery = Column(Integer) total_count = Column(Integer) workload = Column(Float) - created_at = Column(Date, default=func.date(func.now())) + created_at = Column(Date, default=func.current_date(), index=True) + + __table_args__ = ( + UniqueConstraint("office_id", "created_at", name="uq_office_rating"), + ) diff --git a/src/parser/service.py b/src/parser/service.py index 005d5e4..44768b0 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -1,4 +1,5 @@ from typing import List, Set +from datetime import date from pydantic import ValidationError from sqlalchemy import inspect, exc @@ -251,7 +252,8 @@ def create_or_update_ratings( on_conflict_set: Set[str], db: Session, ): - """Добавление рейтинга офиса в базу данных + """Добавление рейтинга офиса в базу данных если дата существует в базе данных + то происходит обновление рейтинга, если даты нет то создается новая запись :param list_in: Список объектов :param index_elements: Индексы элементов для проверки уникальности @@ -261,6 +263,7 @@ def create_or_update_ratings( objs = [] for obj_in in list_in: obj_dict = obj_in.dict(exclude_unset=True) + obj_dict["created_at"] = date.today() objs.append(obj_dict) insert_query = pg_insert(OfficeRatingObject).values(objs) From b1f45acd875b1b9faeccf917c436869835c0aaf6 Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 27 Jun 2024 13:36:10 +0500 Subject: [PATCH 25/28] Fix dump offices to csv --- .../8463e7e99e71_remove_unique_office_id.py | 32 ++++++++ src/initial_data.py | 21 +++-- src/main_bot.py | 79 ++++++++++++++++++- src/parser/models.py | 2 +- src/parser/service.py | 27 +++++++ 5 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 src/alembic/versions/8463e7e99e71_remove_unique_office_id.py diff --git a/src/alembic/versions/8463e7e99e71_remove_unique_office_id.py b/src/alembic/versions/8463e7e99e71_remove_unique_office_id.py new file mode 100644 index 0000000..dc8ef82 --- /dev/null +++ b/src/alembic/versions/8463e7e99e71_remove_unique_office_id.py @@ -0,0 +1,32 @@ +"""Remove unique office_id + +Revision ID: 8463e7e99e71 +Revises: 446dbb27c428 +Create Date: 2024-06-27 07:45:30.375260 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8463e7e99e71' +down_revision: Union[str, None] = '446dbb27c428' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_officeratingobject_office_id', table_name='officeratingobject') + op.create_index(op.f('ix_officeratingobject_office_id'), 'officeratingobject', ['office_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_officeratingobject_office_id'), table_name='officeratingobject') + op.create_index('ix_officeratingobject_office_id', 'officeratingobject', ['office_id'], unique=True) + # ### end Alembic commands ### diff --git a/src/initial_data.py b/src/initial_data.py index 2674c69..7c62c2e 100644 --- a/src/initial_data.py +++ b/src/initial_data.py @@ -2,13 +2,15 @@ from db.session import SessionLocal import csv + from parser.models import OfficeObject # set up logging logger = WBLogger(__name__).get_logger() + with SessionLocal() as db: - with open('processed_data.csv', 'r', encoding='utf-8-sig') as file: + with open("processed_data.csv", "r", encoding="utf-8-sig") as file: csv_reader = csv.reader(file) headers = next(csv_reader) @@ -17,16 +19,18 @@ name = row[1].strip('"') company = row[2] manager = row[3] - office_area = int(row[4]) if row[4] else 0 + office_area = float(row[4]) if row[4] else 0.0 rent = float(row[5]) salary_rate = float(row[6]) min_wage = int(row[7]) internet = int(row[8]) administration = int(row[9]) - existing_office = db.query(OfficeObject).filter_by(office_id=office_id).first() + existing_office = ( + db.query(OfficeObject).filter_by(office_id=office_id).first() + ) if not existing_office: - logger.info('Initial new data to OfficeObject') + logger.info("Initial new data to OfficeObject") office_object = OfficeObject( office_id=office_id, name=name, @@ -37,12 +41,14 @@ salary_rate=salary_rate, min_wage=min_wage, internet=internet, - administration=administration + administration=administration, ) db.add(office_object) - logger.info(f'Office ID {office_object.office_id} with name {office_object.name} was added') + logger.info( + f"Office ID {office_object.office_id} with name {office_object.name} was added" + ) else: - logger.info('Trying to update office data...') + logger.info("Trying to update office data...") existing_office.name = name existing_office.company = company existing_office.manager = manager @@ -54,4 +60,3 @@ existing_office.administration = administration db.commit() - diff --git a/src/main_bot.py b/src/main_bot.py index b6bc7ac..557aa9b 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -1,3 +1,4 @@ +import csv import itertools from warnings import filterwarnings @@ -48,6 +49,7 @@ update_office_field, add_office, create_or_update_ratings, + get_all_offices_full, ) sys.path.append(os.path.join(os.path.dirname(__file__), "src")) @@ -65,9 +67,15 @@ ) -OFFICES_MENU, VIEW_OFFICES, ADD_OFFICE, SELECT_OFFICE, EDIT_OFFICE, DELETE_OFFICE = ( - range(7, 13) -) +( + OFFICES_MENU, + VIEW_OFFICES, + ADD_OFFICE, + SELECT_OFFICE, + EDIT_OFFICE, + DELETE_OFFICE, + DUMP_OFFICE_CSV, +) = range(7, 14) # Shortcut for ConversationHandler.END @@ -113,6 +121,9 @@ async def offices(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: text="Просмотреть офисы", callback_data=str(VIEW_OFFICES) ), InlineKeyboardButton(text="Добавить офис", callback_data=str(ADD_OFFICE)), + InlineKeyboardButton( + text="Сохранить офисы в CSV", callback_data=str(DUMP_OFFICE_CSV) + ), ] ] keyboard = InlineKeyboardMarkup(buttons) @@ -456,6 +467,63 @@ async def save_new_office(update: Update, context: ContextTypes.DEFAULT_TYPE): return VIEW_OFFICES +async def export_offices_to_csv(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Экспорт данных в CSV""" + logger.info("Export offices to CSV begin") + with get_db() as db: + office_data = get_all_offices_full(db_session=db) + if not office_data: + await context.bot.send_message( + chat_id=update.effective_chat.id, text="Нет данных для экспорта." + ) + return OFFICES_MENU + + csv_file_path = "processed_data.csv" + try: + with open(csv_file_path, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file, quoting=csv.QUOTE_MINIMAL) + headers = [ + "office_id", + "name", + "company", + "manager", + "office_area", + "rent", + "salary_rate", + "min_wage", + "internet", + "administration", + ] + writer.writerow(headers) + for office in office_data: + + writer.writerow( + [ + office["office_id"], + f'"{office["name"]}"', + office["company"], + office["manager"], + office["office_area"], + office["rent"], + office["salary_rate"], + office["min_wage"], + office["internet"], + office["administration"], + ] + ) + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Данные успешно экспортированы в файл {csv_file_path}.", + ) + except Exception as e: + logger.error(f"Ошибка при записи в CSV файл: {e}") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Произошла ошибка при записи данных в CSV файл.", + ) + return OFFICES_MENU + + ################################# ### Работа с API WB async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -777,7 +845,7 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: def main() -> None: - application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() + application = ApplicationBuilder().token(TELEGRAM_TOKEN2).build() logger.info("Main function") conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], @@ -830,6 +898,9 @@ def main() -> None: CallbackQueryHandler( add_office_handler, pattern="^" + str(ADD_OFFICE) + "$" ), + CallbackQueryHandler( + export_offices_to_csv, pattern="^" + str(DUMP_OFFICE_CSV) + "$" + ), ], VIEW_OFFICES: [ CallbackQueryHandler(select_office, pattern="^office_"), diff --git a/src/parser/models.py b/src/parser/models.py index f343af0..7fe644f 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -57,7 +57,7 @@ class OfficeRatingObject(Base): """Office ratings""" id = Column(Integer, primary_key=True, index=True) - office_id = Column(Integer, unique=True, index=True) + office_id = Column(Integer, index=True) office_name = Column(String) avg_hours = Column(Float) avg_hours_by_region = Column(Float) diff --git a/src/parser/service.py b/src/parser/service.py index 44768b0..5ae7119 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -78,6 +78,33 @@ def get_all_offices(db_session): return [] +def get_all_offices_full(db_session): + """Получение всех офисов со всеми полями""" + + try: + offices = db_session.query(OfficeObject).all() + office_data = [ + { + "office_id": office.office_id, + "name": office.name, + "company": office.company, + "manager": office.manager, + "office_area": office.office_area, + "rent": office.rent, + "salary_rate": office.salary_rate, + "min_wage": office.min_wage, + "internet": office.internet, + "administration": office.administration, + } + for office in offices + ] + + return office_data + except Exception as e: + logger.error(f"Error in get_all_offices_full: {e}") + return [] + + def get_office_info(db_session, office_id): """Получение офиса констант по office_id""" From 2f3b1b2f4054b43cb6d358fc0af8733e7df1b2ad Mon Sep 17 00:00:00 2001 From: deymonster Date: Thu, 27 Jun 2024 13:41:51 +0500 Subject: [PATCH 26/28] Fix token --- src/main_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main_bot.py b/src/main_bot.py index 557aa9b..5a428f9 100644 --- a/src/main_bot.py +++ b/src/main_bot.py @@ -845,7 +845,7 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: def main() -> None: - application = ApplicationBuilder().token(TELEGRAM_TOKEN2).build() + application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() logger.info("Main function") conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], From a41dd3eeb5e41311761095a7448e2c483e05ca81 Mon Sep 17 00:00:00 2001 From: deymonster Date: Sat, 29 Jun 2024 16:45:09 +0500 Subject: [PATCH 27/28] Fix empty office_id and name in Shortages --- src/parser/api.py | 780 +++++++++++++++++++++++++++------------------- 1 file changed, 460 insertions(+), 320 deletions(-) diff --git a/src/parser/api.py b/src/parser/api.py index 031f04d..a487c20 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -3,7 +3,14 @@ import io from parser.service import safe_sale_object_to_db -from utils.env import base_url_v2, base_url_v1, refresh_url, operations_url, shortages_url, shks_url +from utils.env import ( + base_url_v2, + base_url_v1, + refresh_url, + operations_url, + shortages_url, + shks_url, +) from datetime import datetime, timedelta import pandas as pd from typing import List, Optional @@ -17,25 +24,27 @@ @dataclass class FoundInfo: """Датакласс для foundinfo""" + found_in_office_id: int found_at: datetime found_by_employee_id: int operation: str @classmethod - def from_dict(cls, data: Dict) -> 'FoundInfo': + def from_dict(cls, data: Dict) -> "FoundInfo": """Создание объекта FoundInfo из словаря""" return cls( - found_in_office_id=data['found_in_office_id'], - found_at=datetime.fromisoformat(data['found_at']), - found_by_employee_id=data['found_by_employee_id'], - operation=data['operation'] + found_in_office_id=data["found_in_office_id"], + found_at=datetime.fromisoformat(data["found_at"]), + found_by_employee_id=data["found_by_employee_id"], + operation=data["operation"], ) @dataclass class Shk: """Датакласс для хранения ШК""" + amount: int found_info: Optional[FoundInfo] item_name: str @@ -45,22 +54,27 @@ class Shk: shk_id: int @classmethod - def from_dict(cls, data: Dict) -> 'Shk': + def from_dict(cls, data: Dict) -> "Shk": """Создание объекта Shk из словаря""" return cls( - amount=data['amount'], - found_info=FoundInfo.from_dict(data['found_info']) if data.get('found_info') else None, - item_name=data['item_name'], - item_photo_url=data['item_photo_url'], - item_site_url=data['item_site_url'], - new_shk_id=data['new_shk_id'] if data.get('new_shk_id') else None, - shk_id=data['shk_id'] + amount=data["amount"], + found_info=( + FoundInfo.from_dict(data["found_info"]) + if data.get("found_info") + else None + ), + item_name=data["item_name"], + item_photo_url=data["item_photo_url"], + item_site_url=data["item_site_url"], + new_shk_id=data["new_shk_id"] if data.get("new_shk_id") else None, + shk_id=data["shk_id"], ) @dataclass class ResponseShk: """Дата класс для ответа по shortage_id""" + shks: List[Shk] = field(default_factory=list) @staticmethod @@ -69,14 +83,13 @@ def from_dict(cls, data: Dict): shks_list = data.get("shks", []) if shks_list and isinstance(shks_list, list): shks_list = [Shk.from_dict(item) for item in shks_list] - return cls( - shks=shks_list - ) + return cls(shks=shks_list) @dataclass class Shortage: """Дата класс для хранения недостач""" + shortage_id: int create_dt: datetime guilty_employee_id: int @@ -88,39 +101,40 @@ class Shortage: shks_data: Optional[ResponseShk] = None @classmethod - def from_dict(cls, data: Dict) -> 'Shortage': + def from_dict(cls, data: Dict) -> "Shortage": """Создание объекта из словаря""" - shks_data = data.get('shks_data') + shks_data = data.get("shks_data") return cls( - shortage_id=data['shortage_id'], - create_dt=datetime.fromisoformat(data['create_dt']), - guilty_employee_id=data['guilty_employee_id'], - guilty_employee_name=data['guilty_employee_name'], - amount=data['amount'], - comment=data['comment'], - status_id=data['status_id'], - is_history_exist=data['is_history_exist'], - shks_data=ResponseShk.from_dict(shks_data) if shks_data else None + shortage_id=data["shortage_id"], + create_dt=datetime.fromisoformat(data["create_dt"]), + guilty_employee_id=data["guilty_employee_id"], + guilty_employee_name=data["guilty_employee_name"], + amount=data["amount"], + comment=data["comment"], + status_id=data["status_id"], + is_history_exist=data["is_history_exist"], + shks_data=ResponseShk.from_dict(shks_data) if shks_data else None, ) @dataclass class OfficeShortage: - """ Дата класс для хранения офиса с недостачами""" - office_id: int - office_name: str + """Дата класс для хранения офиса с недостачами""" + office_amount: float + office_id: int = 0 + office_name: str = "Unknown" shortages: List[Shortage] = field(default_factory=list) @classmethod - def from_dict(cls, data: Dict) -> 'OfficeShortage': + def from_dict(cls, data: Dict) -> "OfficeShortage": """Создание объекта из словаря""" shortages_list = data.get("shortages", []) if shortages_list and isinstance(shortages_list, list): shortages_list = [Shortage.from_dict(item) for item in shortages_list] return cls( - office_id=data["office_id"], - office_name=data["office_name"], + office_id=data.get("office_id", 0), + office_name=data.get("office_name", "Unknown"), office_amount=data["office_amount"], shortages=shortages_list, ) @@ -129,6 +143,7 @@ def from_dict(cls, data: Dict) -> 'OfficeShortage': @dataclass class Employee: """Класс для хранения данных о сотруднике""" + employee_id: int last_name: str first_name: str @@ -138,17 +153,17 @@ class Employee: rating: float @classmethod - def from_dict(cls, data: Dict) -> 'Employee': + def from_dict(cls, data: Dict) -> "Employee": """Создание объекта из словаря""" - if not data.get('is_deleted'): + if not data.get("is_deleted"): return cls( - employee_id=data['employee_id'], - last_name=data['last_name'], - first_name=data['first_name'], - middle_name=data['middle_name'], - phone=data['phones'][0] if data.get('phones') else None, - create_date=datetime.fromisoformat(data['create_date']), - rating=data['rating'] + employee_id=data["employee_id"], + last_name=data["last_name"], + first_name=data["first_name"], + middle_name=data["middle_name"], + phone=data["phones"][0] if data.get("phones") else None, + create_date=datetime.fromisoformat(data["create_date"]), + rating=data["rating"], ) else: return None @@ -157,6 +172,7 @@ def from_dict(cls, data: Dict) -> 'Employee': @dataclass class EmployeeOperations: """Класс для хранения операций сотрудника""" + date: str on_place_cnt: int return_count: int @@ -169,42 +185,46 @@ def set_barcode(self, barcode: str): self.barcode = barcode @classmethod - def from_dict(cls, data: Dict) -> 'EmployeeOperations': + def from_dict(cls, data: Dict) -> "EmployeeOperations": """Создание объекта класса из словаря""" return cls( - date=data['date'], - on_place_cnt=data['on_place_cnt'], - return_count=data['return_count'], - return_sum=data['return_sum'], - sale_count=data['sale_count'], - sale_sum=data['sale_sum'], - barcode=None) + date=data["date"], + on_place_cnt=data["on_place_cnt"], + return_count=data["return_count"], + return_sum=data["return_sum"], + sale_count=data["sale_count"], + sale_sum=data["sale_sum"], + barcode=None, + ) # класс для хранения операций @dataclass class Operation: """Класс для хранения операций""" + # dt: datetime # поле для хранения даты операции oper_type: str # тип операции oper_amount: float # cумма операции comment: Optional[str] = None # комментарий - тут обычно ШК товара - grouped: List['Operation'] = field(default_factory=list) # дополнительный вложенный список операций + grouped: List["Operation"] = field( + default_factory=list + ) # дополнительный вложенный список операций @classmethod - def from_dict(cls, data: Dict) -> 'Operation': - """Создание объекта из словаря""""" - oper_type = oper_type_mapping.get(data.get('oper_type', ''), '') + def from_dict(cls, data: Dict) -> "Operation": + """Создание объекта из словаря""" "" + oper_type = oper_type_mapping.get(data.get("oper_type", ""), "") grouped = [] - grouped_data = data.get('grouped', []) + grouped_data = data.get("grouped", []) if grouped_data and isinstance(grouped_data, list): grouped = [cls.from_dict(item) for item in grouped_data] return cls( # dt=datetime.fromisoformat(data.get('dt', '')), oper_type=oper_type, - oper_amount=data.get('oper_amount', 0), - comment=data.get('comment', None), - grouped=grouped + oper_amount=data.get("oper_amount", 0), + comment=data.get("comment", None), + grouped=grouped, ) def to_dict(self): @@ -216,16 +236,19 @@ def to_dict(self): @dataclass class OperationsByDate: """Класс для хранения операций по дате""" + date: datetime operations: List[Operation] @classmethod - def from_dict(cls, data: Dict) -> 'OperationsByDate': + def from_dict(cls, data: Dict) -> "OperationsByDate": """Создание объекта из словаря""" return cls( - date=datetime.fromisoformat(data.get('date', '')), - operations=[Operation.from_dict(item) for item in data.get('operations', [])] + date=datetime.fromisoformat(data.get("date", "")), + operations=[ + Operation.from_dict(item) for item in data.get("operations", []) + ], ) def to_dict(self): @@ -237,10 +260,7 @@ def to_dict(self): class Office: """Класс для хранения офисов""" - def __init__(self, - id: int, - name: str, - office_shk: str): + def __init__(self, id: int, name: str, office_shk: str): self.id = id self.name = name self.office_shk = office_shk @@ -250,23 +270,24 @@ def __init__(self, class SaleData: """Класс для хранения данных о продажах""" - def __init__(self, - office_id: int, # id офиса - name: str, # наименование офиса - date: datetime, # дата - sale_sum: int, # продажи - sale_count: int, # количество продаж - return_sum: int, # возвраты - return_count: int, # количество возвратов - proceeds: int, # вознаграждения - amount: int, # объем продаж - bags_sum: int, # пакеты - office_rating: float, # рейтинг ПВЗ - percent: int, # тариф ставка грейд - office_rating_sum: int, # сумма рейтинга - supplier_return_sum: int, # сумма возвратов - office_speed_sum: float # скорость - ): + def __init__( + self, + office_id: int, # id офиса + name: str, # наименование офиса + date: datetime, # дата + sale_sum: int, # продажи + sale_count: int, # количество продаж + return_sum: int, # возвраты + return_count: int, # количество возвратов + proceeds: int, # вознаграждения + amount: int, # объем продаж + bags_sum: int, # пакеты + office_rating: float, # рейтинг ПВЗ + percent: int, # тариф ставка грейд + office_rating_sum: int, # сумма рейтинга + supplier_return_sum: int, # сумма возвратов + office_speed_sum: float, # скорость + ): self.office_id = office_id self.name = name self.date = date @@ -286,21 +307,23 @@ def __init__(self, def to_dict(self): return self.__dict__ + class OfficeRates: """Класс для хранения показателей офисов""" - def __init__(self, - office_id: int, # id офиса - avg_rate: float, # рейтинг - avg_region_rate: float, # рейтинг региона - avg_hours: float, # время раскладки в часах - avg_hours_by_region: float, # время раскладки в часах по региону - inbox_count: int, # товаров в коробках - limit_delivery: int, # лимит доставок - total_count: int, # товаров на пвз - workload: float, # загруженность заказами - office_name: str = None, # наименование офиса - ): + def __init__( + self, + office_id: int, # id офиса + avg_rate: float, # рейтинг + avg_region_rate: float, # рейтинг региона + avg_hours: float, # время раскладки в часах + avg_hours_by_region: float, # время раскладки в часах по региону + inbox_count: int, # товаров в коробках + limit_delivery: int, # лимит доставок + total_count: int, # товаров на пвз + workload: float, # загруженность заказами + office_name: str = None, # наименование офиса + ): self.office_id = office_id self.avg_rate = avg_rate self.avg_region_rate = avg_region_rate @@ -315,6 +338,7 @@ def __init__(self, def to_dict(self): return self.__dict__ + class ParserWB: """Parser for WB API""" @@ -366,10 +390,12 @@ def _get_response_data_wb(self, *, url: str, params: dict = None, prefix: str): def _get_supplier_id(self): """Get supplier id from wb api - получение ID - supplier_id работника""" url = f"{self.base_url_v1}/account" - params = {'in_short': 'false'} + params = {"in_short": "false"} try: - if response := self._get_response_data_wb(url=url, params=params, prefix='auth'): - self.supplier_id = response['supplier_id'] + if response := self._get_response_data_wb( + url=url, params=params, prefix="auth" + ): + self.supplier_id = response["supplier_id"] except Exception as e: logger.error(e) @@ -380,10 +406,12 @@ def get_offices_ids(self): def fetch_employees(self): """Get list of employees from wb api - Получение списка сотрудников и запись экземпляр класса""" url = f"{self.base_url_v1}/account" - params = {'in_short': 'false'} + params = {"in_short": "false"} try: - if response := self._get_response_data_wb(url=url, params=params, prefix='employees'): - for employee_data in response['employees']: + if response := self._get_response_data_wb( + url=url, params=params, prefix="employees" + ): + for employee_data in response["employees"]: employee = Employee.from_dict(employee_data) # check if employee is not deleted if employee: @@ -400,70 +428,97 @@ def fetch_employee_data(self, date_from: datetime, date_to: datetime) -> List[Di :return: List of dict with employee operations """ url = f"{self.base_url_v2}/employees/proceeds" - logger.info(f'Url - {url}') + logger.info(f"Url - {url}") # get employee list ids from self.employees - получаем список id всех пользователей employee_ids = [employee.employee_id for employee in self.employees] - logger.info(f'Employee ids - {employee_ids}') + logger.info(f"Employee ids - {employee_ids}") params = { - 'employee_ids': ','.join(map(str, employee_ids)), - 'from': date_from, - 'to': date_to, - 'employee_type': 0 + "employee_ids": ",".join(map(str, employee_ids)), + "from": date_from, + "to": date_to, + "employee_type": 0, } all_operations = [] try: - if response := self._get_response_data_wb(url=url, params=params, prefix='employees_operations'): + if response := self._get_response_data_wb( + url=url, params=params, prefix="employees_operations" + ): for employee_data in response: operations_employee = [] - for data in (employee_data.get('by_date') or []): + for data in employee_data.get("by_date") or []: operation = EmployeeOperations.from_dict(data) if operation: operations_employee.append(operation) - employee_info = next((e for e in self.employees if e.employee_id == employee_data['employee_id']), - None) - all_operations.append({ - 'employee_id': employee_data['employee_id'], - 'last_name': employee_info.last_name if employee_info else None, - 'first_name': employee_info.first_name if employee_info else None, - 'middle_name': employee_info.middle_name if employee_info else None, - 'phone': employee_info.phone if employee_info else None, - 'create_date': employee_info.create_date if employee_info else None, - 'rating': employee_info.rating if employee_info else None, - 'operations': operations_employee - }) + employee_info = next( + ( + e + for e in self.employees + if e.employee_id == employee_data["employee_id"] + ), + None, + ) + all_operations.append( + { + "employee_id": employee_data["employee_id"], + "last_name": ( + employee_info.last_name if employee_info else None + ), + "first_name": ( + employee_info.first_name if employee_info else None + ), + "middle_name": ( + employee_info.middle_name if employee_info else None + ), + "phone": employee_info.phone if employee_info else None, + "create_date": ( + employee_info.create_date if employee_info else None + ), + "rating": employee_info.rating if employee_info else None, + "operations": operations_employee, + } + ) except Exception as e: logger.error(e) return all_operations def fetch_shortages_data(self, date_from: str = None, date_to: str = None): - """Fetch all shortages from all offices """ + """Fetch all shortages from all offices""" result = [] - date_from = datetime.strptime(date_from, '%Y-%m-%d') - date_to = datetime.strptime(date_to, '%Y-%m-%d') - if shortages_response := self._get_response_data_wb(url=shortages_url, prefix='shortages'): - - for data in (shortages_response.get('offices') or []): - if data.get('office_id') == 0: + date_from = datetime.strptime(date_from, "%Y-%m-%d") + date_to = datetime.strptime(date_to, "%Y-%m-%d") + if shortages_response := self._get_response_data_wb( + url=shortages_url, prefix="shortages" + ): + + for data in shortages_response.get("offices") or []: + office_id = data.get("office_id", 0) + office_name = data.get("office_name", "Unknown") + if not office_id: logger.warning(f"Неизвестный офис {data.get('office_id')}") - continue # skip empty office + # continue # skip empty office + if not office_name: + logger.warning(f"Неизвестный офис с пустым office_name") # Преобразование JSON в объект OfficeShortage shortages_office_data = OfficeShortage.from_dict(data) # Применение фильтрации по датам, если они были переданы if date_from and date_to: filtered_shortages = [ - shortage for shortage in shortages_office_data.shortages - if date_from <= shortage.create_dt.replace(tzinfo=None) <= date_to + shortage + for shortage in shortages_office_data.shortages + if date_from + <= shortage.create_dt.replace(tzinfo=None) + <= date_to ] for shortage in filtered_shortages: - if shks_response := self._get_response_data_wb(url=shks_url, - params={'shortage_id': shortage.shortage_id}, - prefix='shks'): - shks_list = shks_response.get('shks', []) + if shks_response := self._get_response_data_wb( + url=shks_url, + params={"shortage_id": shortage.shortage_id}, + prefix="shks", + ): + shks_list = shks_response.get("shks", []) shks_objects = [Shk.from_dict(item) for item in shks_list] - shks_data = ResponseShk( - shks=shks_objects - ) + shks_data = ResponseShk(shks=shks_objects) shortage.shks_data = shks_data @@ -473,7 +528,7 @@ def fetch_shortages_data(self, date_from: str = None, date_to: str = None): office_id=shortages_office_data.office_id, office_name=shortages_office_data.office_name, office_amount=shortages_office_data.office_amount, - shortages=filtered_shortages + shortages=filtered_shortages, ) ) else: @@ -483,15 +538,25 @@ def fetch_shortages_data(self, date_from: str = None, date_to: str = None): def fetch_offices(self): """Get offices from wb api - Получение всех офисов из API WB""" url = f"{self.base_url_v1}/account" - params = {'in_short': 'false'} + params = {"in_short": "false"} try: - if response := self._get_response_data_wb(url=url, params=params, prefix='office'): + if response := self._get_response_data_wb( + url=url, params=params, prefix="office" + ): self._get_supplier_id() - for office in response['offices']: - logger.info(f"Office ID: {office['id']}, Office name: {office['name']}") - if office['is_site_active'] is False: + for office in response["offices"]: + logger.info( + f"Office ID: {office['id']}, Office name: {office['name']}" + ) + if office["is_site_active"] is False: continue - self.offices.append(Office(id=office['id'], name=office['name'], office_shk=office['office_shk'])) + self.offices.append( + Office( + id=office["id"], + name=office["name"], + office_shk=office["office_shk"], + ) + ) except Exception as e: logger.error(e) @@ -499,35 +564,41 @@ def fetch_offices(self): def fetch_offices_rates(self, office_ids: List[int]) -> None: """Fetch rates of all offices""" url_rates = f"{self.base_url_v1}/office/rates" - params = {'office_ids': ','.join(map(str, office_ids))} + params = {"office_ids": ",".join(map(str, office_ids))} url_speed = f"{self.base_url_v1}/office/on-place" url_workload = f"{self.base_url_v1}/office/info/workload" merged_data = {} try: - if response := self._get_response_data_wb(url=url_rates, params=params, prefix='office_rates'): - logger.info('Office rates received successfully') - office_rates = response + if response := self._get_response_data_wb( + url=url_rates, params=params, prefix="office_rates" + ): + logger.info("Office rates received successfully") + office_rates = response # Обработка списка рейтингов for item in office_rates: - office_id = item['office_id'] + office_id = item["office_id"] if office_id not in merged_data: merged_data[office_id] = {} merged_data[office_id].update(item) - if response := self._get_response_data_wb(url=url_speed, params=params, prefix='office_on_place'): - logger.info('Offices speed received successfully') + if response := self._get_response_data_wb( + url=url_speed, params=params, prefix="office_on_place" + ): + logger.info("Offices speed received successfully") offices_speed = response # Обработка списка скорости офисов for item in offices_speed: - office_id = item['office_id'] + office_id = item["office_id"] if office_id not in merged_data: merged_data[office_id] = {} merged_data[office_id].update(item) - if response := self._get_response_data_wb(url=url_workload, params=params, prefix='office_info'): - logger.info('Offices workload received successfully') - offices_workload = response + if response := self._get_response_data_wb( + url=url_workload, params=params, prefix="office_info" + ): + logger.info("Offices workload received successfully") + offices_workload = response # Обработка списка производительности for item in offices_workload: - office_id = item['office_id'] + office_id = item["office_id"] if office_id not in merged_data: merged_data[office_id] = {} merged_data[office_id].update(item) @@ -538,95 +609,121 @@ def fetch_offices_rates(self, office_ids: List[int]) -> None: except Exception as e: logger.error(e) - - - def fetch_sales_data(self, date_from=None, date_to=None) -> Union[ - list[Any], list[SaleData]]: - """Get sales data from wb api - Получение данных по продажам """ + def fetch_sales_data( + self, date_from=None, date_to=None + ) -> Union[list[Any], list[SaleData]]: + """Get sales data from wb api - Получение данных по продажам""" result = [] - date_from = date_from or (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') - date_to = date_to or datetime.now().strftime('%Y-%m-%d') + date_from = date_from or (datetime.now() - timedelta(days=30)).strftime( + "%Y-%m-%d" + ) + date_to = date_to or datetime.now().strftime("%Y-%m-%d") params = { # 'office_ids': office_id, - 'from': date_from, - 'to': date_to + "from": date_from, + "to": date_to, } for office in self.offices: office_id = office.id - params['office_ids'] = office_id + params["office_ids"] = office_id url_sales = f"{self.base_url_v2}/proceeds" url_reward = f"{self.base_url_v1}/accruals" # запрос данных по продажам sales_data_dict = {} sales_data = [] - if sale_response := self._get_response_data_wb(url=url_sales, params=params, prefix='sales'): + if sale_response := self._get_response_data_wb( + url=url_sales, params=params, prefix="sales" + ): try: sales_data = sale_response[0] - if sales_data['by_office'] is None: + if sales_data["by_office"] is None: continue - sales_data_dict = {sale['date']: sale for sale in sales_data['by_office']} + sales_data_dict = { + sale["date"]: sale for sale in sales_data["by_office"] + } except Exception as e: - logger.error(f'Ошибка при обработке данных для офиса {office_id}: {e}') + logger.error( + f"Ошибка при обработке данных для офиса {office_id}: {e}" + ) return [] # запрос данных по вознаграждениям rewards_data_dict = {} - if reward_response := self._get_response_data_wb(url=url_reward, params=params, prefix='reward'): - rewards_data_dict = {reward['date']: reward for reward in reward_response} + if reward_response := self._get_response_data_wb( + url=url_reward, params=params, prefix="reward" + ): + rewards_data_dict = { + reward["date"]: reward for reward in reward_response + } for date, sale in sales_data_dict.items(): reward_data = rewards_data_dict.get(date, None) - amount = reward_data['amount'] if reward_data else 0 - bags_sum = reward_data['ext_data']['bags_sum'] if reward_data else 0 - office_rating = reward_data['ext_data']['office_rating'] if reward_data else 0 - percent = reward_data['ext_data']['percent'][0] if reward_data else 0 - office_rating_sum = reward_data['ext_data']['office_rating_sum'] if reward_data else 0 - supplier_return_sum = reward_data['ext_data']['supplier_return_sum'] if reward_data else 0 - office_speed_sum = reward_data['ext_data']['office_speed_sum'] if reward_data else 0 + amount = reward_data["amount"] if reward_data else 0 + bags_sum = reward_data["ext_data"]["bags_sum"] if reward_data else 0 + office_rating = ( + reward_data["ext_data"]["office_rating"] if reward_data else 0 + ) + percent = reward_data["ext_data"]["percent"][0] if reward_data else 0 + office_rating_sum = ( + reward_data["ext_data"]["office_rating_sum"] if reward_data else 0 + ) + supplier_return_sum = ( + reward_data["ext_data"]["supplier_return_sum"] if reward_data else 0 + ) + office_speed_sum = ( + reward_data["ext_data"]["office_speed_sum"] if reward_data else 0 + ) if office_speed_sum is None: office_speed_sum = 0 sale_object = SaleData( office_id=office_id, - name=sales_data['office_name'], - date=datetime.strptime(date, '%Y-%m-%d'), - sale_count=sale['sale_count'], - return_count=sale['return_count'], - sale_sum=sale['sale_sum'], - return_sum=sale['return_sum'], - proceeds=sale['proceeds'], + name=sales_data["office_name"], + date=datetime.strptime(date, "%Y-%m-%d"), + sale_count=sale["sale_count"], + return_count=sale["return_count"], + sale_sum=sale["sale_sum"], + return_sum=sale["return_sum"], + proceeds=sale["proceeds"], amount=amount, bags_sum=bags_sum, office_rating=office_rating, percent=percent, office_rating_sum=office_rating_sum, supplier_return_sum=supplier_return_sum, - office_speed_sum=office_speed_sum - + office_speed_sum=office_speed_sum, ) - logger.info(f'Обработана дата {date} для офиса {office_id} -- {sale_object.name}') + logger.info( + f"Обработана дата {date} для офиса {office_id} -- {sale_object.name}" + ) result.append(sale_object) - logger.info('Begin to write data to DB') + logger.info("Begin to write data to DB") safe_sale_object_to_db(sale_objects=result) return result - def fetch_operations_data(self, date_from: datetime = None, date_to: datetime = None): + def fetch_operations_data( + self, date_from: datetime = None, date_to: datetime = None + ): self._get_supplier_id() params = { - 'supplier_id': self.supplier_id, - 'all': 'true', + "supplier_id": self.supplier_id, + "all": "true", } - if operations_response := self._get_response_data_wb(url=operations_url, params=params, prefix='operations'): - details_data = operations_response['details'] + if operations_response := self._get_response_data_wb( + url=operations_url, params=params, prefix="operations" + ): + details_data = operations_response["details"] all_operations = [OperationsByDate.from_dict(item) for item in details_data] # Фильтрация операций по датам, если они были переданы if date_from and date_to: - date_from = datetime.strptime(date_from, '%Y-%m-%d') - date_to = datetime.strptime(date_to, '%Y-%m-%d') - filtered_operations = [op for op in all_operations if date_from <= op.date <= date_to] + date_from = datetime.strptime(date_from, "%Y-%m-%d") + date_to = datetime.strptime(date_to, "%Y-%m-%d") + filtered_operations = [ + op for op in all_operations if date_from <= op.date <= date_to + ] return filtered_operations else: return all_operations @@ -636,59 +733,68 @@ def generate_csv_io(self, data: List[OfficeShortage], filename: str): csv_buffer = io.StringIO() # Создаем объект writer для записи в CSV - csv_writer = csv.DictWriter(csv_buffer, - fieldnames=[ - "Office ID", - "Название офиса", - "Общая недостача офиса", - "ID недостачи", - "Дата недостачи", - "ID сотрудника", - "Ф.И.О сотрудника", - "Сумма недостачи", - "Причина недостачи", - "Status ID", - "is history exists", - "Стоимость недостачи по ШК", - "Наименование товара", - "URL на фото товара", - "URL товара в каталоге", - "Новый ШК", - "Основной ШК", - "Найдено в офисе ID", - "Дата находки", - "Кем найдено ID сотрудника", - "Наименование операции" - - ]) + csv_writer = csv.DictWriter( + csv_buffer, + fieldnames=[ + "Office ID", + "Название офиса", + "Общая недостача офиса", + "ID недостачи", + "Дата недостачи", + "ID сотрудника", + "Ф.И.О сотрудника", + "Сумма недостачи", + "Причина недостачи", + "Status ID", + "is history exists", + "Стоимость недостачи по ШК", + "Наименование товара", + "URL на фото товара", + "URL товара в каталоге", + "Новый ШК", + "Основной ШК", + "Найдено в офисе ID", + "Дата находки", + "Кем найдено ID сотрудника", + "Наименование операции", + ], + ) csv_writer.writeheader() for office in data: for shortage in office.shortages: for shks_data in shortage.shks_data.shks: found_info = shks_data.found_info - csv_writer.writerow({ - "Office ID": office.office_id, - "Название офиса": office.office_name, - "Общая недостача офиса": office.office_amount, - "ID недостачи": shortage.shortage_id, - "Дата недостачи": shortage.create_dt, - "ID сотрудника": shortage.guilty_employee_id, - "Ф.И.О сотрудника": shortage.guilty_employee_name, - "Сумма недостачи": shortage.amount, - "Причина недостачи": shortage.comment, - "Status ID": shortage.status_id, - "is history exists": shortage.is_history_exist, - "Стоимость недостачи по ШК": shks_data.amount, - "Наименование товара": shks_data.item_name, - "URL на фото товара": shks_data.item_photo_url, - "URL товара в каталоге": shks_data.item_site_url, - "Новый ШК": shks_data.new_shk_id, - "Основной ШК": shks_data.shk_id, - "Найдено в офисе ID": found_info.found_in_office_id if found_info else None, - "Дата находки": found_info.found_at if found_info else None, - "Кем найдено ID сотрудника": found_info.found_by_employee_id if found_info else None, - "Наименование операции": found_info.operation if found_info else None - }) + csv_writer.writerow( + { + "Office ID": office.office_id, + "Название офиса": office.office_name, + "Общая недостача офиса": office.office_amount, + "ID недостачи": shortage.shortage_id, + "Дата недостачи": shortage.create_dt, + "ID сотрудника": shortage.guilty_employee_id, + "Ф.И.О сотрудника": shortage.guilty_employee_name, + "Сумма недостачи": shortage.amount, + "Причина недостачи": shortage.comment, + "Status ID": shortage.status_id, + "is history exists": shortage.is_history_exist, + "Стоимость недостачи по ШК": shks_data.amount, + "Наименование товара": shks_data.item_name, + "URL на фото товара": shks_data.item_photo_url, + "URL товара в каталоге": shks_data.item_site_url, + "Новый ШК": shks_data.new_shk_id, + "Основной ШК": shks_data.shk_id, + "Найдено в офисе ID": ( + found_info.found_in_office_id if found_info else None + ), + "Дата находки": found_info.found_at if found_info else None, + "Кем найдено ID сотрудника": ( + found_info.found_by_employee_id if found_info else None + ), + "Наименование операции": ( + found_info.operation if found_info else None + ), + } + ) csv_buffer.seek(0) buf = io.BytesIO() @@ -701,47 +807,50 @@ def generate_csv_io(self, data: List[OfficeShortage], filename: str): return bytes_data - def save_csv_memory(self, - data: List[object], - filename: str, - column_names_mapping: Dict[str, str] - ): + def save_csv_memory( + self, data: List[object], filename: str, column_names_mapping: Dict[str, str] + ): def process_operations(operations, date): for operation in operations: if isinstance(operation, Operation): op_dict = operation.to_dict() else: op_dict = operation - op_dict['date'] = date - filtered_item_dict = {csv_key: op_dict[obj_key] for obj_key, csv_key in column_names_mapping.items() if - obj_key in op_dict} + op_dict["date"] = date + filtered_item_dict = { + csv_key: op_dict[obj_key] + for obj_key, csv_key in column_names_mapping.items() + if obj_key in op_dict + } csv_writer.writerow(filtered_item_dict) - logger.info(f'Записаны данные в строку файла - {filtered_item_dict}') + logger.info(f"Записаны данные в строку файла - {filtered_item_dict}") # Рекурсивно обрабатываем вложенные операции, если они есть - if 'grouped' in op_dict and op_dict['grouped']: - process_operations(op_dict['grouped'], date) + if "grouped" in op_dict and op_dict["grouped"]: + process_operations(op_dict["grouped"], date) # Получаем имена столбцов из словаря column_names_mapping column_names = list(column_names_mapping.values()) # Объект в памяти для записи CSV csv_buffer = io.StringIO() # Создаем объект writer для записи в CSV - csv_writer = csv.DictWriter(csv_buffer, - fieldnames=column_names) + csv_writer = csv.DictWriter(csv_buffer, fieldnames=column_names) csv_writer.writeheader() for item in data: item_dict = item.__dict__ # обработка операций - if 'operations' in item_dict and item_dict['operations']: - process_operations(item_dict['operations'], item_dict['date']) + if "operations" in item_dict and item_dict["operations"]: + process_operations(item_dict["operations"], item_dict["date"]) else: - filtered_item_dict = {csv_key: item_dict[obj_key] for obj_key, csv_key in column_names_mapping.items() - if obj_key in item_dict} + filtered_item_dict = { + csv_key: item_dict[obj_key] + for obj_key, csv_key in column_names_mapping.items() + if obj_key in item_dict + } csv_writer.writerow(filtered_item_dict) - logger.info(f'Записаны данные в строку файла - {filtered_item_dict}') + logger.info(f"Записаны данные в строку файла - {filtered_item_dict}") csv_buffer.seek(0) buf = io.BytesIO() @@ -754,89 +863,120 @@ def process_operations(operations, date): bytes_data = buf.getvalue() return bytes_data - def save_to_csv(self, - data: List[object], - filename: str, - column_names_mapings: Dict[str, str], - oper_type_mapings: Dict[int, str] = None): + def save_to_csv( + self, + data: List[object], + filename: str, + column_names_mapings: Dict[str, str], + oper_type_mapings: Dict[int, str] = None, + ): data_to_save = [item.to_dict() for item in data] df = pd.DataFrame(data_to_save) # Проверка на наличие колонки operations - if 'operations' in df.columns: - df = df.explode('operations') + if "operations" in df.columns: + df = df.explode("operations") # Разбиваем операции на отдельные колонки - operations_df = pd.DataFrame(df['operations'].to_list()) + operations_df = pd.DataFrame(df["operations"].to_list()) df = pd.concat( - [df.drop(['operations'], axis=1).reset_index(drop=True), operations_df.reset_index(drop=True)], axis=1) + [ + df.drop(["operations"], axis=1).reset_index(drop=True), + operations_df.reset_index(drop=True), + ], + axis=1, + ) if oper_type_mapings: - df['oper_type'] = df['oper_type'].map(oper_type_mapings) + df["oper_type"] = df["oper_type"].map(oper_type_mapings) - if 'grouped' in df.columns: - df = df.explode('grouped') - grouped_df = pd.DataFrame(df['grouped'].dropna().to_list()) - df = pd.concat([df.drop(['grouped'], axis=1).reset_index(drop=True), grouped_df.reset_index(drop=True)], - axis=1) + if "grouped" in df.columns: + df = df.explode("grouped") + grouped_df = pd.DataFrame(df["grouped"].dropna().to_list()) + df = pd.concat( + [ + df.drop(["grouped"], axis=1).reset_index(drop=True), + grouped_df.reset_index(drop=True), + ], + axis=1, + ) df.rename(columns=column_names_mapings, inplace=True) df.to_csv(filename, index=False) return None - def safe_to_csv_operations(self, - data: List[OperationsByDate], - filename: str): + def safe_to_csv_operations(self, data: List[OperationsByDate], filename: str): data_to_save = [] for item in data: for operation in item.operations: operations = [operation] operations.extend(operation.grouped) for op in operations: - data_to_save.append({ - 'date': item.date, - 'oper_type': op.oper_type, - 'oper_amount': op.oper_amount, - 'comment': op.comment, - 'grouped': None - }) + data_to_save.append( + { + "date": item.date, + "oper_type": op.oper_type, + "oper_amount": op.oper_amount, + "comment": op.comment, + "grouped": None, + } + ) df = pd.DataFrame(data_to_save) - df['comment'] = df['comment'].str.replace('\D+', '', regex=True) + df["comment"] = df["comment"].str.replace("\D+", "", regex=True) # rename "" "" - df.rename(columns={ - 'date': 'Дата', - 'oper_type': 'Тип операции', - 'oper_amount': 'Сумма', - 'comment': 'Комментарий', - }, inplace=True) + df.rename( + columns={ + "date": "Дата", + "oper_type": "Тип операции", + "oper_amount": "Сумма", + "comment": "Комментарий", + }, + inplace=True, + ) df.to_csv(filename, index=False) def save_to_csv_mananagers_operations(self, data: List[Dict], filename: str): - with open(filename, 'w', newline='') as csvfile: - fieldnames = ['ID сотрудника', 'Фамилия', 'Имя', 'Отчество', 'Телефон', 'Дата трудоустройства', 'Рейтинг', - 'Дата операции', 'Принято вещей', 'Возвраты', 'Возвраты (сумма)', 'Продажи', - 'Продажи (сумма)', - 'ШК офиса'] + with open(filename, "w", newline="") as csvfile: + fieldnames = [ + "ID сотрудника", + "Фамилия", + "Имя", + "Отчество", + "Телефон", + "Дата трудоустройства", + "Рейтинг", + "Дата операции", + "Принято вещей", + "Возвраты", + "Возвраты (сумма)", + "Продажи", + "Продажи (сумма)", + "ШК офиса", + ] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for employee in data: - for operation in employee['operations']: + for operation in employee["operations"]: if operation and operation.date: - writer.writerow({ - 'ID сотрудника': employee['employee_id'], - 'Фамилия': employee['last_name'], - 'Имя': employee['first_name'], - 'Отчество': employee['middle_name'], - 'Телефон': employee['phone'], - 'Дата трудоустройства': employee['create_date'], - 'Рейтинг': employee['rating'], - 'Дата операции': operation.date, - 'Принято вещей': operation.on_place_cnt, - 'Возвраты': operation.return_count, - 'Возвраты (сумма)': operation.return_sum, - 'Продажи': operation.sale_count, - 'Продажи (сумма)': operation.sale_sum, - 'ШК офиса': operation.barcode - }) + writer.writerow( + { + "ID сотрудника": employee["employee_id"], + "Фамилия": employee["last_name"], + "Имя": employee["first_name"], + "Отчество": employee["middle_name"], + "Телефон": employee["phone"], + "Дата трудоустройства": employee["create_date"], + "Рейтинг": employee["rating"], + "Дата операции": operation.date, + "Принято вещей": operation.on_place_cnt, + "Возвраты": operation.return_count, + "Возвраты (сумма)": operation.return_sum, + "Продажи": operation.sale_count, + "Продажи (сумма)": operation.sale_sum, + "ШК офиса": operation.barcode, + } + ) else: - logger.info(f'Нет данных по операциям для сотрудника {employee["employee_id"]}') + logger.info( + f'Нет данных по операциям для сотрудника {employee["employee_id"]}' + ) From f07c48057c02ecd2a06b61b9d74b92c49a2e9ea4 Mon Sep 17 00:00:00 2001 From: deymonster Date: Wed, 16 Oct 2024 09:05:23 +0500 Subject: [PATCH 28/28] Add rate by region --- src/alembic/versions/5d857f1d71be_.py | 30 ++++++++++ src/parser/api.py | 9 ++- src/parser/auth_fr.py | 63 ++++++++++----------- src/parser/column_names.py | 80 +++++++++++++-------------- src/parser/models.py | 3 +- src/parser/schemas.py | 1 + src/parser/service.py | 2 + 7 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 src/alembic/versions/5d857f1d71be_.py diff --git a/src/alembic/versions/5d857f1d71be_.py b/src/alembic/versions/5d857f1d71be_.py new file mode 100644 index 0000000..d04529e --- /dev/null +++ b/src/alembic/versions/5d857f1d71be_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 5d857f1d71be +Revises: 8463e7e99e71 +Create Date: 2024-10-14 10:46:43.773605 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5d857f1d71be' +down_revision: Union[str, None] = '8463e7e99e71' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('saleobject', sa.Column('rate_by_region', sa.Float(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('saleobject', 'rate_by_region') + # ### end Alembic commands ### diff --git a/src/parser/api.py b/src/parser/api.py index a487c20..25eb692 100644 --- a/src/parser/api.py +++ b/src/parser/api.py @@ -283,6 +283,7 @@ def __init__( amount: int, # объем продаж bags_sum: int, # пакеты office_rating: float, # рейтинг ПВЗ + rate_by_region: float, # рейтинг средний по региону percent: int, # тариф ставка грейд office_rating_sum: int, # сумма рейтинга supplier_return_sum: int, # сумма возвратов @@ -299,6 +300,7 @@ def __init__( self.amount = amount self.bags_sum = bags_sum self.office_rating = office_rating + self.rate_by_region = rate_by_region self.percent = percent self.office_rating_sum = office_rating_sum self.supplier_return_sum = supplier_return_sum @@ -356,9 +358,6 @@ def __init__(self, session): self.headers = { "Accept": "application/json.txt, text/plain, */*", "Referer": "https://franchise.wildberries.ru/", - "sec-ch-ua-mobile": "?0", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", - "sec-ch-ua-platform": "Windows", } self.session.headers.update(self.headers) @@ -664,6 +663,9 @@ def fetch_sales_data( office_rating = ( reward_data["ext_data"]["office_rating"] if reward_data else 0 ) + rate_by_region = ( + reward_data["ext_data"]["rate_by_region"] if reward_data else 0 + ) percent = reward_data["ext_data"]["percent"][0] if reward_data else 0 office_rating_sum = ( reward_data["ext_data"]["office_rating_sum"] if reward_data else 0 @@ -688,6 +690,7 @@ def fetch_sales_data( amount=amount, bags_sum=bags_sum, office_rating=office_rating, + rate_by_region=rate_by_region, percent=percent, office_rating_sum=office_rating_sum, supplier_return_sum=supplier_return_sum, diff --git a/src/parser/auth_fr.py b/src/parser/auth_fr.py index 44b91cb..e24b741 100644 --- a/src/parser/auth_fr.py +++ b/src/parser/auth_fr.py @@ -18,7 +18,7 @@ class Auth: "Referer": "https://franchise.wildberries.ru/", "Content-Type": "application/x-www-form-urlencoded", "Authorization": f"Basic {basic}", - "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0" + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0", } def __init__(self): @@ -34,7 +34,10 @@ def get_auth_status(self): return self.auth_status def get_franchise_session(self, phone: str): - + """Get franchise session + :param str phone: phone number + :return requests.Session: session + """ self.refresh_token = self._load_refresh_token(phone) logger.info(f"Refresh token is {self.refresh_token}") # Init status code @@ -42,24 +45,28 @@ def get_franchise_session(self, phone: str): # If refresh token is not None then connect with refresh token if self.refresh_token: self.session, status_code = self.connect_with_token() - logger.info(f"Status code after connect with refresh token is {status_code}") + logger.info( + f"Status code after connect with refresh token is {status_code}" + ) logger.info(f"Session after connect with refresh token is {self.session}") if self.session and status_code != 400: logger.info("Session is OK") return self.session else: self._remove_refresh_token_file(phone) - logger.error(" Refresh token is expired. Trying to connect with phone and code") + logger.error( + " Refresh token is expired. Trying to connect with phone and code" + ) # If status code is 400 then refresh token is expired # else connect with phone and code - params = { - 'phone': phone - } + params = {"phone": phone} try: - response = self.session.get(url=LOGIN_FR_URL, headers=self.headers, params=params) + response = self.session.get( + url=LOGIN_FR_URL, headers=self.headers, params=params + ) logger.info(f"Response for login franchise is {response.json()}") - if response.status_code == 200 and response.json()['isSuccess']: + if response.status_code == 200 and response.json()["isSuccess"]: logger.info(f"Response for login franchise is OK") # send code from LK WB and pass it to _connect_with_code # code = input('Enter code from LK WB: ') @@ -68,7 +75,7 @@ def get_franchise_session(self, phone: str): # else: self.auth_status = "NEED_CODE" else: - self.auth_status = response.json()['message'] + self.auth_status = response.json()["message"] except Exception as e: logger.error(f"Error while login to franchise {e}") self.auth_status = "ERROR" @@ -84,17 +91,13 @@ def _remove_refresh_token_file(self, phone): logger.error(f"Error while removing refresh token file {e}") def connect_with_code(self, phone, code): - payload = { - 'grant_type': 'password', - 'username': phone, - 'password': code - } + payload = {"grant_type": "password", "username": phone, "password": code} try: response = self.session.post(url=TOKEN_URL, data=payload) if response.status_code == 200: - self.token = response.json()['access_token'] - self.session.headers.update({'Authorization': f"Bearer {self.token}"}) - self.refresh_token = response.json()['refresh_token'] + self.token = response.json()["access_token"] + self.session.headers.update({"Authorization": f"Bearer {self.token}"}) + self.refresh_token = response.json()["refresh_token"] self._save_refresh_token(self.refresh_token, phone) # os.environ['refresh_token'] = self.refresh_token logger.info(f"New Refresh token is taken {self.refresh_token}") @@ -107,19 +110,18 @@ def connect_with_code(self, phone, code): return None def connect_with_token(self): - payload = { - 'refresh_token': self.refresh_token, - 'grant_type': 'refresh_token' - } + payload = {"refresh_token": self.refresh_token, "grant_type": "refresh_token"} logger.info(f"Payload for refresh token is {payload}") try: - response = self.session.post(url=self.token_url, data=payload, headers=self.headers) + response = self.session.post( + url=self.token_url, data=payload, headers=self.headers + ) if response.status_code == 200: - self.token = response.json()['access_token'] - self.session.headers.update({'Authorization': f"Bearer {self.token}"}) + self.token = response.json()["access_token"] + self.session.headers.update({"Authorization": f"Bearer {self.token}"}) return self.session, response.status_code else: - logger.error(f'Status code: {response.status_code}') + logger.error(f"Status code: {response.status_code}") return self.session, response.status_code except Exception as e: logger.error(f"Error while connecting with token {e}") @@ -127,17 +129,16 @@ def connect_with_token(self): def _save_refresh_token(self, refresh_token, phone): date_str = datetime.now().strftime("%d-%m-%Y_%H:%M:%S") - filename = f'refresh_token_{phone}_{date_str}.txt' - with open(filename, 'w') as f: + filename = f"refresh_token_{phone}_{date_str}.txt" + with open(filename, "w") as f: f.write(refresh_token) def _load_refresh_token(self, phone): try: - files = glob.glob(f'refresh_token_{phone}_*.txt') + files = glob.glob(f"refresh_token_{phone}_*.txt") latest_file = max(files, key=os.path.getctime) - with open(latest_file, 'r') as f: + with open(latest_file, "r") as f: return f.read() except Exception as e: logger.error(f"Error while loading refresh token {e}") return None - diff --git a/src/parser/column_names.py b/src/parser/column_names.py index 355510c..4f2b855 100644 --- a/src/parser/column_names.py +++ b/src/parser/column_names.py @@ -1,42 +1,41 @@ - sale_data_column_names_mapping = { - 'office_id': 'ID офиса', - 'name': 'Название офиса', - 'date': 'Дата', - 'sale_count': 'Кол-во продаж', - 'return_count': 'Кол-во возвратов', - 'sale_sum': 'Продажи РУБ', - 'return_sum': 'Возвраты РУБ', - 'proceeds': 'Объем продаж РУБ', - 'amount': 'Вознаграждение РУБ', - 'bags_sum': 'Пакеты РУБ', - 'office_rating': 'Рейтинг ПВЗ', - 'percent': 'Тариф (ставка грейда)', - 'office_rating_sum': 'Сумма рейтинга', - 'supplier_return_sum': 'Сумма возвратов поставщика', - 'office_speed_sum': 'Скорость' - + "office_id": "ID офиса", + "name": "Название офиса", + "date": "Дата", + "sale_count": "Кол-во продаж", + "return_count": "Кол-во возвратов", + "sale_sum": "Продажи РУБ", + "return_sum": "Возвраты РУБ", + "proceeds": "Объем продаж РУБ", + "amount": "Вознаграждение РУБ", + "bags_sum": "Пакеты РУБ", + "office_rating": "Рейтинг ПВЗ", + "rate_by_region": "Средний рейтинг по региону", + "percent": "Тариф (ставка грейда)", + "office_rating_sum": "Сумма рейтинга", + "supplier_return_sum": "Сумма возвратов поставщика", + "office_speed_sum": "Скорость", } -office_rates_column_names_mapping = { - 'office_id': 'ID офиса', - 'office_name': 'Название офиса', - 'avg_hours': 'Время раскладки', - 'avg_hours_by_region': 'Среднее время раскладки по региону', - 'avg_rate': 'Рейтинг филиала', - 'avg_rate_by_region': 'Средний рейтинг по региону', - 'inbox_count': 'Товаров в коробках', - 'limit_delivery': 'Лимит доставок', - 'total_count': 'Товаров на ПВЗ', - 'workload': 'Загруженность товарами' +office_rates_column_names_mapping = { + "office_id": "ID офиса", + "office_name": "Название офиса", + "avg_hours": "Время раскладки", + "avg_hours_by_region": "Среднее время раскладки по региону", + "avg_rate": "Рейтинг филиала", + "avg_rate_by_region": "Средний рейтинг по региону", + "inbox_count": "Товаров в коробках", + "limit_delivery": "Лимит доставок", + "total_count": "Товаров на ПВЗ", + "workload": "Загруженность товарами", } operations_data_column_names_mapping = { - 'date': 'Дата', - 'oper_type': 'Тип операции', - 'oper_amount': 'Сумма операции', - 'comment': 'ШК', + "date": "Дата", + "oper_type": "Тип операции", + "oper_amount": "Сумма операции", + "comment": "ШК", } shortages_data_column_names_mapping = { @@ -50,16 +49,15 @@ "amount": "Сумма недостачи", "comment": "Причина недостачи", "status_id": "Status ID", - "is_history_exist": "is history exists - история?" + "is_history_exist": "is history exists - история?", } - -oper_type_mapping= { - 1: 'Недостача', - 2: 'Премирование', - 3: 'Депремирование', - 4: 'Брак ШК/Коллективная ответственность', - 5: 'Вывод средств на реквизиты', - 6: 'Вознаграждения по продажам' +oper_type_mapping = { + 1: "Недостача", + 2: "Премирование", + 3: "Депремирование", + 4: "Брак ШК/Коллективная ответственность", + 5: "Вывод средств на реквизиты", + 6: "Вознаграждения по продажам", } diff --git a/src/parser/models.py b/src/parser/models.py index 7fe644f..4f44e55 100644 --- a/src/parser/models.py +++ b/src/parser/models.py @@ -20,7 +20,7 @@ class OfficeObject(Base): class SaleObject(Base): - """Model poll""" + """Model saleobject""" id = Column(Integer, primary_key=True, index=True) office_id = Column(Integer) @@ -36,6 +36,7 @@ class SaleObject(Base): amount = Column(Integer) bags_sum = Column(Integer) office_rating = Column(Float) + rate_by_region = Column(Float) percent = Column(Float) office_rating_sum = Column(Integer) supplier_return_sum = Column(Integer) diff --git a/src/parser/schemas.py b/src/parser/schemas.py index e2d900f..e7ec7e2 100644 --- a/src/parser/schemas.py +++ b/src/parser/schemas.py @@ -17,6 +17,7 @@ class SaleObjectIn(BaseModel): amount: int bags_sum: int office_rating: float + rate_by_region: float percent: float office_rating_sum: int supplier_return_sum: int diff --git a/src/parser/service.py b/src/parser/service.py index 5ae7119..10e183e 100644 --- a/src/parser/service.py +++ b/src/parser/service.py @@ -32,6 +32,7 @@ def convert_sale_data_to_sale_object_in( amount, bags_sum, office_rating, + rate_by_region, percent, office_rating_sum, supplier_return_sum, @@ -49,6 +50,7 @@ def convert_sale_data_to_sale_object_in( amount=int(amount), bags_sum=bags_sum, office_rating=round(float(office_rating), 3), + rate_by_region=round(float(rate_by_region), 3), percent=round(float(percent), 2), office_rating_sum=int(office_rating_sum), supplier_return_sum=supplier_return_sum,