diff --git a/app.py b/app.py index 288c205..1cca8b6 100644 --- a/app.py +++ b/app.py @@ -1,30 +1,23 @@ -from app import MongoDB, aggregate_salaries, Settings +from app import configs, configure_logger, router import asyncio -import json +from aiogram import Bot, Dispatcher +from aiogram.enums.parse_mode import ParseMode +from aiogram.fsm.storage.memory import MemoryStorage +from loguru import logger +def register_logger(): + configure_logger(capture_exceptions=True) + logger.info("Success logger register") -settings = Settings() -client = MongoDB(str(settings.DB_URI)) -db = client.client[settings.DATABASE_NAME] -collection = db[settings.COLLECTION_NAME] - -json_str = ''' -{ - "dt_from": "2022-09-01T00:00:00", - "dt_upto": "2022-12-31T23:59:00", - "group_type": "month" -} -''' - -async def main(json_str): - data = json.loads(json_str) - dt_from = data["dt_from"] - dt_upto = data["dt_upto"] - group_type = data["group_type"] - - result = await aggregate_salaries(collection, dt_from, dt_upto, group_type) - - print(result['dataset'], len(result['dataset'])) - print(result['labels'], len(result['labels'])) - -asyncio.run(main(json_str)) \ No newline at end of file +async def start_app(): + bot = Bot(token=configs.API_TOKEN_TG) + dp = Dispatcher(storage=MemoryStorage()) + dp.include_router(router) + logger.info("Starting bot..") + await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(start_app()) + loop.run_forever() \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 920a29a..24238b6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,2 +1,3 @@ -from app.config import Settings -from app.database import MongoDB, aggregate_salaries \ No newline at end of file +from app.config import configs +from app.logger import configure_logger +from app.handlers import router \ No newline at end of file diff --git a/app/config.py b/app/config.py index b6ff5a7..e7c7981 100644 --- a/app/config.py +++ b/app/config.py @@ -3,7 +3,7 @@ from pydantic import computed_field, MongoDsn from pydantic_core import Url -class Settings(BaseSettings): +class Configs(BaseSettings): API_TOKEN_TG: str HOST_MONGODB: str DATABASE_NAME: str @@ -17,4 +17,6 @@ class Settings(BaseSettings): f"mongodb+srv://{self.USERNAME_MONGO}:{self.PASSWORD_MONGO}@{self.HOST_MONGODB}" ) - model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') \ No newline at end of file + model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') + +configs = Configs() \ No newline at end of file diff --git a/app/database/MongoDBConfig.py b/app/database/MongoDBConfig.py new file mode 100644 index 0000000..9cba3f8 --- /dev/null +++ b/app/database/MongoDBConfig.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class MongoDBConfig(BaseModel): + url: str + db_name: str = None + collection: str = None \ No newline at end of file diff --git a/app/database/mongodb.py b/app/database/mongodb.py index 4c3bf9b..2ab8fd0 100644 --- a/app/database/mongodb.py +++ b/app/database/mongodb.py @@ -1,5 +1,19 @@ from motor.motor_asyncio import AsyncIOMotorClient +from app.database.MongoDBConfig import MongoDBConfig + class MongoDB: - def __init__(self, url: str): - self.client = AsyncIOMotorClient(url) + def __init__(self, config: MongoDBConfig): + self.client = AsyncIOMotorClient(config.url) + self.db = self.get_db(config.db_name) + self.collection = self.get_collection(config.collection) + + def get_db(self, db_name: str): + if db_name is None: + return None + return self.client[db_name] + + def get_collection(self, collection_name: str): + if collection_name is None or self.db is None: + return None + return self.db[collection_name] \ No newline at end of file diff --git a/app/handlers.py b/app/handlers.py new file mode 100644 index 0000000..1dce343 --- /dev/null +++ b/app/handlers.py @@ -0,0 +1,30 @@ +import json +from app.database import MongoDB, aggregate_salaries +from app.config import configs +from aiogram import Router, types +from aiogram.filters import Command +from app.texts import invalid, greetings +from app.query import Query +from app.database.MongoDBConfig import MongoDBConfig +from loguru import logger + + +router = Router() +client_config = MongoDBConfig(url=str(configs.DB_URI), db_name=configs.DATABASE_NAME, collection=configs.COLLECTION_NAME) +client = MongoDB(config=client_config) + +@router.message(Command("start")) +async def start_handler(message: types.Message): + await message.answer(greetings.format(name=message.from_user.first_name)) + + +@router.message() +async def query_handler(message: types.Message): + try: + query = Query(**json.loads(message.text)) + logger.info("Aggregate query starting") + result = await aggregate_salaries(client.collection, query.dt_from, query.dt_upto, query.group_type) + logger.info("Aggregate query complete") + await message.answer(str(result)) + except ValueError as e: + await message.answer(invalid) \ No newline at end of file diff --git a/app/logger.py b/app/logger.py new file mode 100644 index 0000000..634cb99 --- /dev/null +++ b/app/logger.py @@ -0,0 +1,52 @@ +import logging +import sys + +from loguru import logger + + +class InterceptHandler(logging.Handler): + LEVELS_MAP = { + logging.CRITICAL: "CRITICAL", + logging.ERROR: "ERROR", + logging.WARNING: "WARNING", + logging.INFO: "INFO", + logging.DEBUG: "DEBUG", + } + + def _get_level(self, record): + return self.LEVELS_MAP.get(record.levelno, record.levelno) + + def emit(self, record): + logger_opt = logger.opt(depth=6, exception=record.exc_info) + logger_opt.log(self._get_level(record), record.getMessage()) + + +def configure_logger(capture_exceptions: bool = False) -> None: + logger.remove() + level = "INFO" + logger.add( + "logs/log_{time:YYYY-MM-DD}.log", + rotation="12:00", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {file}:{line} | {message}", + level="INFO", + encoding="utf-8", + compression="zip", + ) + logger.add( + sys.stdout, + colorize=True, + format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {file}:{line} | " + "{message}", + level=level, + ) + if capture_exceptions: + logger.add( + "logs/error_log_{time:YYYY-MM-DD}.log", + rotation="12:00", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {file}:{line} | {message}", + level="ERROR", + encoding="utf-8", + compression="zip", + ) + + logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO) diff --git a/app/query.py b/app/query.py new file mode 100644 index 0000000..eb7cc74 --- /dev/null +++ b/app/query.py @@ -0,0 +1,32 @@ +from datetime import datetime +from pydantic import BaseModel, validator + + +class Query(BaseModel): + dt_from: str + dt_upto: str + group_type: str + + @validator('group_type', pre=True, always=True) + def validate_group_type(cls, v): + if v not in ['month', 'day', 'hour']: + raise ValueError('Invalid group_type') + return v + + @validator('dt_from', pre=True, always=True) + def validate_dt_from(cls, v): + try: + datetime.strptime(v, '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise ValueError('Invalid dt_from') + return v + + @validator('dt_upto', pre=True, always=True) + def validate_dt_upto(cls, v, values): + try: + datetime.strptime(v, '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise ValueError('Invalid dt_upto') + if values['dt_from'] > v: + raise ValueError('dt_upto should be later than dt_from') + return v \ No newline at end of file diff --git a/app/texts.py b/app/texts.py new file mode 100644 index 0000000..59de8c8 --- /dev/null +++ b/app/texts.py @@ -0,0 +1,2 @@ +invalid = 'Невалидный запрос. Пример запроса: {"dt_from": "2022-09-01T00:00:00", "dt_upto": "2022-12-31T23:59:00", "group_type": "month"}' +greetings = 'Привет, {name}! Отправьте запрос в формате {{"dt_from": "2022-09-01T00:00:00", "dt_upto": "2022-12-31T23:59:00", "group_type": "month" || "day" || "hour"}}'