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 fastapi import APIRouter, status
from src.service import Checking_imei_service, Authentication_service 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 from src.utils.exceptions import AccessDenied
@ -13,13 +11,10 @@ checking_router = APIRouter()
@checking_router.post( @checking_router.post(
"/check-imei", "/check-imei",
status_code=status.HTTP_200_OK, 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): if not await Authentication_service.token_is_valid(token=body.token):
raise AccessDenied("token_is_not_valid") raise AccessDenied("token_is_not_valid")
report: Union[CheckingReport | dict] = await Checking_imei_service().request_imei_info(body.imei) report: CheckingReport = await Checking_imei_service().request_imei_info(body.imei)
if type(report) is dict: return report.properties
return report
else:
return report.properties

View File

@ -1,7 +1,13 @@
from fastapi import APIRouter, FastAPI, Request from fastapi import APIRouter, FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse 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 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"]) 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) @app.exception_handler(AccessDenied)
async def login_exception_handler(request: Request, exc: AccessDenied): async def token_is_bad(request: Request, exc: AccessDenied):
return JSONResponse( return JSONResponse(
status_code=401, 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 CheckingInput as CheckingInput
from .checking import CheckingOutput as CheckingOutput
from .external_checking import CheckingRequest as CheckingRequest from .external_checking import CheckingRequest as CheckingRequest
from .external_checking import CheckingReport as CheckingReport from .external_checking import CheckingReport as CheckingReport
from .external_checking import DeviceProperties as DeviceProperties from .external_checking import DeviceProperties as DeviceProperties

View File

@ -1,12 +1,6 @@
from typing import Any from pydantic import BaseModel, ConfigDict, field_validator
from pydantic import BaseModel, ConfigDict
from src.utils.exceptions import MyValidationError
class CheckingOutput(BaseModel):
model_config = ConfigDict(from_attributes=True)
result: Any
class CheckingInput(BaseModel): class CheckingInput(BaseModel):
@ -14,3 +8,14 @@ class CheckingInput(BaseModel):
imei: str imei: str
token: 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 meid: Optional[str] = None # parameter isn't exist in documentation
imei2: 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 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 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 repairCoverage: Union[bool, str] # I got bool, in documentation - str
technicalSupport: 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 replacement: Optional[bool] = None # parameter isn't exist in documentation
demoUnit: bool demoUnit: Optional[bool] = None
refurbished: bool refurbished: Optional[bool] = None
purchaseCountry: str purchaseCountry: Optional[str] = None
fmiOn: bool fmiOn: Optional[bool] = None
lostMode: Union[bool, str] # I got bool, in documentation - str lostMode: Union[bool, str, None] = None # I got bool, in documentation - str
loaner: bool # parameter isn't exist in documentation loaner: Optional[bool] = None
usaBlockStatus: Optional[str] = None # parameter is exist only in documentation usaBlockStatus: Optional[str] = None
network: Optional[str] = None # parameter is exist only in documentation network: Optional[str] = None

View File

@ -2,6 +2,8 @@ from aiohttp import ClientSession
from src.settings import settings from src.settings import settings
from src.schemas import CheckingRequest, CheckingReport from src.schemas import CheckingRequest, CheckingReport
from pydantic import ValidationError
from src.utils.exceptions import UnsuccessfullRequest, ExternalDataValidationError
class Checking_imei_service: class Checking_imei_service:
@ -19,6 +21,7 @@ class Checking_imei_service:
deviceId=imei, deviceId=imei,
serviceId=self.service_id serviceId=self.service_id
) )
async with ClientSession() as session: async with ClientSession() as session:
async with session.post( async with session.post(
self.service_url, self.service_url,
@ -27,9 +30,16 @@ class Checking_imei_service:
) as response: ) as response:
try: try:
raw_response = await response.json() raw_response = await response.json()
return CheckingReport(**raw_response)
except Exception as e: except Exception as e:
print(type(e), str(e)) print(type(e), str(e))
print(raw_response)
# TODO: make good handling of request errors # 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 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()