From 44efa841a2d3c3f2121a3fe0839a5c1dc7e5d0f1 Mon Sep 17 00:00:00 2001 From: matv864 Date: Fri, 24 Jan 2025 18:23:45 +1000 Subject: [PATCH] add bot and modify api --- api/src/api/check_imei.py | 15 ++++-------- api/src/app.py | 35 ++++++++++++++++++++++++---- api/src/schemas/__init__.py | 1 - api/src/schemas/checking.py | 21 ++++++++++------- api/src/schemas/external_checking.py | 20 ++++++++-------- api/src/service/check_imei.py | 14 +++++++++-- api/src/utils/exceptions.py | 8 ++++++- bot/.gitignore | 10 ++++++++ bot/.python-version | 1 + bot/README.md | 7 ++++++ bot/pyproject.toml | 30 ++++++++++++++++++++++++ bot/src/__init__.py | 2 ++ bot/src/bot.py | 7 ++++++ bot/src/dispatcher.py | 5 ++++ bot/src/handlers/__init__.py | 2 ++ bot/src/handlers/ask_imei.py | 22 +++++++++++++++++ bot/src/handlers/welcome.py | 11 +++++++++ bot/src/main.py | 15 ++++++++++++ bot/src/services/__init__.py | 1 + bot/src/services/external_request.py | 20 ++++++++++++++++ bot/src/settings.py | 13 +++++++++++ 21 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 bot/.gitignore create mode 100644 bot/.python-version create mode 100644 bot/README.md create mode 100644 bot/pyproject.toml create mode 100644 bot/src/__init__.py create mode 100644 bot/src/bot.py create mode 100644 bot/src/dispatcher.py create mode 100644 bot/src/handlers/__init__.py create mode 100644 bot/src/handlers/ask_imei.py create mode 100644 bot/src/handlers/welcome.py create mode 100644 bot/src/main.py create mode 100644 bot/src/services/__init__.py create mode 100644 bot/src/services/external_request.py create mode 100644 bot/src/settings.py diff --git a/api/src/api/check_imei.py b/api/src/api/check_imei.py index fb8eaf8..be231eb 100644 --- a/api/src/api/check_imei.py +++ b/api/src/api/check_imei.py @@ -1,9 +1,7 @@ -from typing import Union - from fastapi import APIRouter, status from src.service import Checking_imei_service, Authentication_service -from src.schemas import CheckingInput, CheckingOutput, CheckingReport, DeviceProperties +from src.schemas import CheckingInput, CheckingReport, DeviceProperties from src.utils.exceptions import AccessDenied @@ -13,13 +11,10 @@ checking_router = APIRouter() @checking_router.post( "/check-imei", status_code=status.HTTP_200_OK, - response_model=Union[CheckingOutput | dict] + response_model=DeviceProperties ) -async def check_imei(body: CheckingInput) -> Union[DeviceProperties | dict]: +async def check_imei(body: CheckingInput): if not await Authentication_service.token_is_valid(token=body.token): raise AccessDenied("token_is_not_valid") - report: Union[CheckingReport | dict] = await Checking_imei_service().request_imei_info(body.imei) - if type(report) is dict: - return report - else: - return report.properties \ No newline at end of file + report: CheckingReport = await Checking_imei_service().request_imei_info(body.imei) + return report.properties \ No newline at end of file diff --git a/api/src/app.py b/api/src/app.py index 1ee6106..33b5a4c 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -1,7 +1,13 @@ from fastapi import APIRouter, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware + from starlette.responses import JSONResponse -from src.utils.exceptions import AccessDenied +from src.utils.exceptions import ( + AccessDenied, + MyValidationError, + UnsuccessfullRequest, + ExternalDataValidationError +) from src.api import checking_router @@ -21,12 +27,33 @@ main_app_router = APIRouter(prefix="/api") main_app_router.include_router(checking_router, tags=["Check IMEI"]) -app.include_router(checking_router) +app.include_router(main_app_router) @app.exception_handler(AccessDenied) -async def login_exception_handler(request: Request, exc: AccessDenied): +async def token_is_bad(request: Request, exc: AccessDenied): return JSONResponse( status_code=401, - content={}, + content={"detail": "bad token"}, ) + +@app.exception_handler(MyValidationError) +async def input_data_is_invalid(request: Request, exc: MyValidationError): + return JSONResponse( + status_code=400, + content={"detail": str(exc)[:50]}, + ) + +@app.exception_handler(UnsuccessfullRequest) +async def unsuccessfull_request(request: Request, exc: UnsuccessfullRequest): + return JSONResponse( + status_code=404, + content={"detail": "unsuccess"}, + ) + +@app.exception_handler(ExternalDataValidationError) +async def external_data_validation_error(request: Request, exc: ExternalDataValidationError): + return JSONResponse( + status_code=500, + content={"detail": "data from external service is invalid"}, + ) \ No newline at end of file diff --git a/api/src/schemas/__init__.py b/api/src/schemas/__init__.py index 576c3ba..c527c7e 100644 --- a/api/src/schemas/__init__.py +++ b/api/src/schemas/__init__.py @@ -1,5 +1,4 @@ from .checking import CheckingInput as CheckingInput -from .checking import CheckingOutput as CheckingOutput from .external_checking import CheckingRequest as CheckingRequest from .external_checking import CheckingReport as CheckingReport from .external_checking import DeviceProperties as DeviceProperties diff --git a/api/src/schemas/checking.py b/api/src/schemas/checking.py index d944065..2d1d35c 100644 --- a/api/src/schemas/checking.py +++ b/api/src/schemas/checking.py @@ -1,12 +1,6 @@ -from typing import Any -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, field_validator - - -class CheckingOutput(BaseModel): - model_config = ConfigDict(from_attributes=True) - - result: Any +from src.utils.exceptions import MyValidationError class CheckingInput(BaseModel): @@ -14,3 +8,14 @@ class CheckingInput(BaseModel): imei: str token: str + + @field_validator('imei') + def is_imei(cls, value): + if type(value) is not str: + raise MyValidationError(f'imei "{value}" must be str type') + if len(value) != 15: + raise MyValidationError(f'imei "{value}" must 15 length') + if not value.isdigit(): + raise MyValidationError(f'imei "{value}" must be only digits') + + return value \ No newline at end of file diff --git a/api/src/schemas/external_checking.py b/api/src/schemas/external_checking.py index ca57fbf..14ca59a 100644 --- a/api/src/schemas/external_checking.py +++ b/api/src/schemas/external_checking.py @@ -30,20 +30,20 @@ class DeviceProperties(BaseModel): meid: Optional[str] = None # parameter isn't exist in documentation imei2: Optional[str] = None # parameter isn't exist in documentation serial: Optional[str] = None # parameter isn't exist in documentation - estPurchaseDate: int + estPurchaseDate: Optional[int] = None simLock: Optional[bool] = None # parameter is exist only in documentation - warrantyStatus: str + warrantyStatus: Optional[str] = None repairCoverage: Union[bool, str] # I got bool, in documentation - str technicalSupport: Union[bool, str] # I got bool, in documentation - str replacement: Optional[bool] = None # parameter isn't exist in documentation - demoUnit: bool - refurbished: bool - purchaseCountry: str - fmiOn: bool - lostMode: Union[bool, str] # I got bool, in documentation - str - loaner: bool # parameter isn't exist in documentation - usaBlockStatus: Optional[str] = None # parameter is exist only in documentation - network: Optional[str] = None # parameter is exist only in documentation + demoUnit: Optional[bool] = None + refurbished: Optional[bool] = None + purchaseCountry: Optional[str] = None + fmiOn: Optional[bool] = None + lostMode: Union[bool, str, None] = None # I got bool, in documentation - str + loaner: Optional[bool] = None + usaBlockStatus: Optional[str] = None + network: Optional[str] = None diff --git a/api/src/service/check_imei.py b/api/src/service/check_imei.py index 1ccc6bc..2268bbd 100644 --- a/api/src/service/check_imei.py +++ b/api/src/service/check_imei.py @@ -2,6 +2,8 @@ from aiohttp import ClientSession from src.settings import settings from src.schemas import CheckingRequest, CheckingReport +from pydantic import ValidationError +from src.utils.exceptions import UnsuccessfullRequest, ExternalDataValidationError class Checking_imei_service: @@ -19,6 +21,7 @@ class Checking_imei_service: deviceId=imei, serviceId=self.service_id ) + async with ClientSession() as session: async with session.post( self.service_url, @@ -27,9 +30,16 @@ class Checking_imei_service: ) as response: try: raw_response = await response.json() - return CheckingReport(**raw_response) except Exception as e: print(type(e), str(e)) + print(raw_response) # TODO: make good handling of request errors - return dict() \ No newline at end of file + if raw_response.get("status") is not None: + if raw_response.get("status") != "successful": + raise UnsuccessfullRequest() + + try: + return CheckingReport(**raw_response) + except ValidationError: + raise ExternalDataValidationError() diff --git a/api/src/utils/exceptions.py b/api/src/utils/exceptions.py index 7c24d2b..f68a66e 100644 --- a/api/src/utils/exceptions.py +++ b/api/src/utils/exceptions.py @@ -1 +1,7 @@ -class AccessDenied(Exception): ... \ No newline at end of file +class AccessDenied(Exception): ... + +class MyValidationError(Exception): ... + +class UnsuccessfullRequest(Exception): ... + +class ExternalDataValidationError(Exception): ... diff --git a/bot/.gitignore b/bot/.gitignore new file mode 100644 index 0000000..ae8554d --- /dev/null +++ b/bot/.gitignore @@ -0,0 +1,10 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv diff --git a/bot/.python-version b/bot/.python-version new file mode 100644 index 0000000..d9506ce --- /dev/null +++ b/bot/.python-version @@ -0,0 +1 @@ +3.12.5 diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..61b2d2c --- /dev/null +++ b/bot/README.md @@ -0,0 +1,7 @@ +# bot + +```.env +BOT_TOKEN= +OUR_API_TOKEN= +CHECKING_ENDPOINT=http(s)://.../api/check-imei +``` diff --git a/bot/pyproject.toml b/bot/pyproject.toml new file mode 100644 index 0000000..b12f916 --- /dev/null +++ b/bot/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "bot" +version = "0.1.0" +description = "Add your description here" +authors = [ + { name = "matv864", email = "matv864@gmail.com" } +] +dependencies = [ + "pydantic-settings>=2.7.1", + "aiogram>=3.17.0", + "pydantic>=2.10.6", + "aiohttp>=3.11.11", + "requests>=2.32.3", +] +readme = "README.md" +requires-python = ">= 3.8" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/bot/src/__init__.py b/bot/src/__init__.py new file mode 100644 index 0000000..0621a2a --- /dev/null +++ b/bot/src/__init__.py @@ -0,0 +1,2 @@ +# to init it first +import src.handlers # noqa \ No newline at end of file diff --git a/bot/src/bot.py b/bot/src/bot.py new file mode 100644 index 0000000..724d731 --- /dev/null +++ b/bot/src/bot.py @@ -0,0 +1,7 @@ +# it's another file to avoid circular imports + +from aiogram import Bot + +from src.settings import settings + +bot = Bot(token=settings.BOT_TOKEN) diff --git a/bot/src/dispatcher.py b/bot/src/dispatcher.py new file mode 100644 index 0000000..a8883f5 --- /dev/null +++ b/bot/src/dispatcher.py @@ -0,0 +1,5 @@ +# it's another file to avoid circular imports + +from aiogram import Dispatcher + +dispatcher = Dispatcher() \ No newline at end of file diff --git a/bot/src/handlers/__init__.py b/bot/src/handlers/__init__.py new file mode 100644 index 0000000..eaafa1c --- /dev/null +++ b/bot/src/handlers/__init__.py @@ -0,0 +1,2 @@ +from .welcome import welcome as welcome +from .ask_imei import ask_imei_info as ask_imei_info \ No newline at end of file diff --git a/bot/src/handlers/ask_imei.py b/bot/src/handlers/ask_imei.py new file mode 100644 index 0000000..eb4528f --- /dev/null +++ b/bot/src/handlers/ask_imei.py @@ -0,0 +1,22 @@ +import json +from aiogram import types, F +from aiogram.utils.formatting import Text, Code + +from src.dispatcher import dispatcher + +from src.services import External_request_service + + + +@dispatcher.message(F.text) +async def ask_imei_info(message: types.Message): + raw_imei = message.text + if len(raw_imei) != 15: + return await message.answer("bad imei - length must be 15") + if not raw_imei.isdigit(): + return await message.answer("bad imei - length must be only digits") + + raw_info = await External_request_service.get_imei_info(raw_imei) + raw_info_json = json.dumps(raw_info, indent=4, ensure_ascii=False) + + return await message.answer(**Text(Code(raw_info_json)).as_kwargs()) \ No newline at end of file diff --git a/bot/src/handlers/welcome.py b/bot/src/handlers/welcome.py new file mode 100644 index 0000000..51eff6a --- /dev/null +++ b/bot/src/handlers/welcome.py @@ -0,0 +1,11 @@ +from aiogram import types +from aiogram.filters.command import Command + +from src.dispatcher import dispatcher + + +@dispatcher.message(Command("start")) +async def welcome(message: types.Message): + await message.answer( + "hello, insert device's IMEI and I say about this device" + ) \ No newline at end of file diff --git a/bot/src/main.py b/bot/src/main.py new file mode 100644 index 0000000..e5d2f65 --- /dev/null +++ b/bot/src/main.py @@ -0,0 +1,15 @@ +import asyncio +import logging + +from src.dispatcher import dispatcher +from src.bot import bot + +logging.basicConfig(level=logging.INFO) + +async def main(): + await dispatcher.start_polling(bot) + + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/bot/src/services/__init__.py b/bot/src/services/__init__.py new file mode 100644 index 0000000..e226325 --- /dev/null +++ b/bot/src/services/__init__.py @@ -0,0 +1 @@ +from .external_request import External_request_service as External_request_service \ No newline at end of file diff --git a/bot/src/services/external_request.py b/bot/src/services/external_request.py new file mode 100644 index 0000000..be03804 --- /dev/null +++ b/bot/src/services/external_request.py @@ -0,0 +1,20 @@ +from typing import Union +from aiohttp import ClientSession + +from src.settings import settings + +class External_request_service: + @staticmethod + async def get_imei_info(imei: str) -> dict[str, Union[str, int, bool]]: + async with ClientSession() as session: + async with session.post( + str(settings.CHECKING_ENDPOINT), + json={"imei": imei, "token": settings.OUR_API_TOKEN}, + ) as response: + try: + raw_response = await response.json() + return raw_response + except Exception as e: + print(type(e), str(e)) + # print(raw_response) + # TODO: make good handling of request errors \ No newline at end of file diff --git a/bot/src/settings.py b/bot/src/settings.py new file mode 100644 index 0000000..7224f87 --- /dev/null +++ b/bot/src/settings.py @@ -0,0 +1,13 @@ +from pydantic import AnyHttpUrl +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", extra="ignore") + + BOT_TOKEN: str + OUR_API_TOKEN: str + CHECKING_ENDPOINT: AnyHttpUrl + + +settings = Settings()