add bot and modify api

This commit is contained in:
Ivanov Matvey 2025-01-24 18:23:45 +10:00
parent 9de2aefb66
commit 44efa841a2
21 changed files with 224 additions and 36 deletions

View File

@ -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:
report: CheckingReport = await Checking_imei_service().request_imei_info(body.imei)
return report.properties

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,7 @@
class AccessDenied(Exception): ...
class MyValidationError(Exception): ...
class UnsuccessfullRequest(Exception): ...
class ExternalDataValidationError(Exception): ...

10
bot/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# python generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# venv
.venv

1
bot/.python-version Normal file
View File

@ -0,0 +1 @@
3.12.5

7
bot/README.md Normal file
View File

@ -0,0 +1,7 @@
# bot
```.env
BOT_TOKEN=
OUR_API_TOKEN=
CHECKING_ENDPOINT=http(s)://.../api/check-imei
```

30
bot/pyproject.toml Normal file
View File

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

2
bot/src/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# to init it first
import src.handlers # noqa

7
bot/src/bot.py Normal file
View File

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

5
bot/src/dispatcher.py Normal file
View File

@ -0,0 +1,5 @@
# it's another file to avoid circular imports
from aiogram import Dispatcher
dispatcher = Dispatcher()

View File

@ -0,0 +1,2 @@
from .welcome import welcome as welcome
from .ask_imei import ask_imei_info as ask_imei_info

View File

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

View File

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

15
bot/src/main.py Normal file
View File

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

View File

@ -0,0 +1 @@
from .external_request import External_request_service as External_request_service

View File

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

13
bot/src/settings.py Normal file
View File

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