Skip to content

Commit

Permalink
Merge pull request #15 from benyaming/aoigram3
Browse files Browse the repository at this point in the history
Aoigram v3
  • Loading branch information
benyaming authored Aug 20, 2024
2 parents 476038b + 0051ccd commit 89bbcb2
Show file tree
Hide file tree
Showing 24 changed files with 541 additions and 1,474 deletions.
23 changes: 11 additions & 12 deletions bus_bot/clients/bus_api/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import logging
from typing import List

from pydantic import parse_obj_as
from pydantic import TypeAdapter
from httpx import AsyncClient

from bus_bot.clients.bus_api.exceptions import exception_by_codes, ApiNotRespondingError, ApiTimeoutError
from bus_bot.clients.bus_api.models import IncomingRoutesResponse, Stop, IncomingRoute
from bus_bot.misc import session
from bus_bot.config import env


Expand All @@ -14,8 +13,8 @@
logger = logging.getLogger('bus_api_client')

TRANSPORT_ICONS = {
'2': '🚄',
'3': '🚌'
2: '🚄',
3: '🚌'
}


Expand All @@ -39,7 +38,7 @@ def _format_lines(routes: list[IncomingRoute]) -> list[str]:
return lines


async def _get_lines_for_station(station_id: int) -> IncomingRoutesResponse:
async def _get_lines_for_station(station_id: int, session: AsyncClient) -> IncomingRoutesResponse:
url = f'{env.API_URL}/siri/get_routes_for_stop/{station_id}'

try:
Expand All @@ -52,7 +51,7 @@ async def _get_lines_for_station(station_id: int) -> IncomingRoutesResponse:
logging.error((resp.read()).decode('utf-8'))
try:
body = resp.json()
except Exception as e:
except Exception as _:
raise ApiTimeoutError
resp.raise_for_status()

Expand All @@ -67,7 +66,7 @@ async def _get_lines_for_station(station_id: int) -> IncomingRoutesResponse:
return arriving_lines


async def find_near_stops(lat: float, lng: float) -> List[Stop]:
async def find_near_stops(lat: float, lng: float, session: AsyncClient) -> list[Stop]:
url = f'{env.API_URL}/stop/near'
params = {'lat': lat, 'lng': lng, 'radius': 200}

Expand All @@ -87,15 +86,15 @@ async def find_near_stops(lat: float, lng: float) -> List[Stop]:
resp.raise_for_status()

data = resp.json()
stops = parse_obj_as(List[Stop], data)
stops = TypeAdapter(list[Stop]).validate_python(data)

# temporary solution to delete stops with same id (such as central stations platforms)
unique_stops = {stop.code: stop for stop in stops}
return list(unique_stops.values())


async def prepare_station_schedule(station_id: int, is_last_update: bool = False) -> str:
arriving_lines = await _get_lines_for_station(station_id)
async def prepare_station_schedule(station_id: int, session: AsyncClient, is_last_update: bool = False) -> str:
arriving_lines = await _get_lines_for_station(station_id, session)
response_lines = [f'<b>{arriving_lines.stop_info.name} ({arriving_lines.stop_info.code})</b>\n']

if arriving_lines.incoming_routes:
Expand All @@ -111,7 +110,7 @@ async def prepare_station_schedule(station_id: int, is_last_update: bool = False
return response


async def get_stop_info(stop_code: int) -> Stop:
async def get_stop_info(stop_code: int, session: AsyncClient) -> Stop:
url = f'{env.API_URL}/stop/by_code/{stop_code}'

try:
Expand Down
50 changes: 23 additions & 27 deletions bus_bot/clients/bus_api/models.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,58 @@
from __future__ import annotations

from datetime import datetime
from typing import List, Optional

from pydantic import BaseModel, Field


__all__ = ['Stop', 'Route', 'IncomingRoute', 'IncomingRoutesResponse', 'StopLocation']


class IncomingRoutesResponse(BaseModel):
response_time: datetime = Field(default_factory=datetime.now)
stop_info: 'Stop'
incoming_routes: List['IncomingRoute']


class Agency(BaseModel):
id: int
name: str
url: str
phone: str


class Stop(BaseModel):
id: int
code: int
name: str
city: str | None
street: Optional[str] = None
floor: Optional[str] = None
platform: Optional[str] = None
location: 'StopLocation'
location_type: str
parent_station_id: Optional[str] = None
zone_id: Optional[str] = None


class Route(BaseModel):
id: str
id: int
agency: Agency
short_name: str
from_stop_name: str
to_stop_name: str
from_city: str
to_city: str
description: str
type: str
type: int
color: str


class IncomingRoute(BaseModel):
eta: int
route: 'Route'
route: Route


class StopLocation(BaseModel):
type: str = 'Point'
coordinates: List[float]
coordinates: list[float]


class Stop(BaseModel):
id: int
code: int
name: str
city: str | None
street: str | None = None
floor: str | None = None
platform: str | None = None
location: StopLocation
location_type: int
parent_station_id: str | None = None
zone_id: int | None = None

IncomingRoutesResponse.update_forward_refs()
Stop.update_forward_refs()

class IncomingRoutesResponse(BaseModel):
response_time: datetime = Field(default_factory=datetime.now)
stop_info: Stop
incoming_routes: list[IncomingRoute]
27 changes: 15 additions & 12 deletions bus_bot/clients/map_generator/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import json
import logging
from io import BytesIO
from typing import List, Tuple
from urllib.parse import quote

from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from httpx import AsyncClient

from bus_bot.config import env
from bus_bot.clients.bus_api.client import find_near_stops
from bus_bot.clients.bus_api.exceptions import NoStopsError
from bus_bot.clients.bus_api.models import Stop
from bus_bot.misc import session
from bus_bot.helpers import CallbackPrefix


Expand All @@ -31,7 +30,7 @@ def _get_marker_color(stop: Stop) -> str:
return color


def _get_encoded_geojson_from_stops(stops: List[Stop]) -> str:
def _get_encoded_geojson_from_stops(stops: list[Stop]) -> str:
features = []
for i, stop in enumerate(stops, 1):
features.append(
Expand All @@ -51,18 +50,22 @@ def _get_encoded_geojson_from_stops(stops: List[Stop]) -> str:
return quote(json.dumps(geojson))


def _get_kb_for_stops(stops: List[Stop]) -> InlineKeyboardMarkup:
kb = InlineKeyboardMarkup()
def _get_kb_for_stops(stops: list[Stop]) -> InlineKeyboardMarkup:
rows = []
for i, stop in enumerate(stops, 1):
kb.row(InlineKeyboardButton(
text=f'{i}{stop.name} ({stop.code})',
callback_data=f'{CallbackPrefix.get_stop}{stop.code}'
))
return kb
rows.append(
[
InlineKeyboardButton(
text=f'{i}{stop.name} ({stop.code})',
callback_data=f'{CallbackPrefix.get_stop}{stop.code}'
)
]
)
return InlineKeyboardMarkup(inline_keyboard=rows)


async def get_map_with_points(lat: float, lng: float) -> Tuple[BytesIO, InlineKeyboardMarkup]:
stops = await find_near_stops(lat, lng)
async def get_map_with_points(lat: float, lng: float, session: AsyncClient) -> tuple[BytesIO, InlineKeyboardMarkup]:
stops = await find_near_stops(lat, lng, session)
if len(stops) == 0:
raise NoStopsError

Expand Down
44 changes: 20 additions & 24 deletions bus_bot/config.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
from typing import Optional

from pydantic import BaseSettings, Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Env(BaseSettings):
class Config:
env_file = '.env'
env_file_encoding = 'utf-8'
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')

TOKEN: str = Field(env='TOKEN')
TOKEN: str

WEBAPP_HOST: Optional[str] = Field(None, env='WEBAPP_HOST')
WEBAPP_PORT: Optional[int] = Field(None, env='WEBAPP_PORT')
WEBHOOK_PATH: Optional[str] = Field(None, env='WEBHOOK_PATH')
WEBHOOK_URL: Optional[str] = Field(None, env='WEBHOOK_URL')
WEBAPP_HOST: str | None = None
WEBAPP_PORT: int | None = None
WEBHOOK_PATH: str | None = None
WEBHOOK_URL: str | None = None

DOCKER_MODE: bool = Field(False, env='DOCKER_MODE')
DOCKER_MODE: bool = False

DB_URL: str = Field('localhost', env='DB_URL')
DB_NAME: str = Field(..., env='DB_NAME')
DB_COLLECTION_NAME: str = Field(..., env='DB_COLLECTION_NAME')
DB_URL: str = 'localhost'
DB_NAME: str
DB_COLLECTION_NAME: str

PERIOD: int = Field(5, env='PERIOD') # how often message updates (seconds)
TTL: int = Field(5, env='TTL') # how long message updates (seconds)
API_URL: str
MAPBOX_TOKEN: str

METRICS_DSN: Optional[str] = Field(None, env='METRICS_DSN')
METRICS_TABLE_NAME: Optional[str] = Field(None, env='METRICS_TABLE_NAME')
PERIOD: int = 5 # how often message updates (seconds)
TTL: int = 5 # how long message updates (seconds)

SENTRY_KEY: Optional[str] = Field(None, env='SENTRY_KEY')
API_URL: str = Field(..., env='API_URL')
MAPBOX_TOKEN: str = Field(..., env='MAPBOX_TOKEN')
THROTTLE_QUANTITY: int = 30
THROTTLE_PERIOD: int = 3

THROTTLE_QUANTITY: int = Field(30, env='THROTTLE_QUANTITY')
THROTTLE_PERIOD: int = Field(3, env='THROTTLE_PERIOD')
METRICS_DSN: str | None = None
METRICS_TABLE_NAME: str | None = None
SENTRY_KEY: str | None = None


env = Env()
78 changes: 24 additions & 54 deletions bus_bot/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,32 @@
from aiogram.dispatcher.filters import Filter, Text
from aiogram.types import ContentTypes, ContentType
from aiogram.utils.exceptions import MessageNotModified
from aiogram import F, Dispatcher
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters.exception import ExceptionTypeFilter

from bus_bot.clients.bus_api.exceptions import StationNonExistsError, ApiNotRespondingError, NoStopsError
from bus_bot.helpers import CallbackPrefix
from bus_bot.misc import dp

from . import forms
from . import errors
from . import helpers
from . import commands
from . import bus_handlers
from bus_bot.clients.bus_api.exceptions import StationNonExistsError, ApiNotRespondingError, NoStopsError
from bus_bot.exceptions import StopAlreadySaved
from bus_bot.handlers import errors
from bus_bot.handlers.forms import router as forms_router
from bus_bot.handlers.helpers import incorrect_message_router, cancel_router
from bus_bot.handlers.commands import router as commands_router
from bus_bot.handlers.bus_handlers import router as bus_handlers_router


__all__ = ['register_handlers']

from .. import texts
from ..exceptions import StopAlreadySaved

from ..states import RenameStopState


def register_handlers():
def register_handlers(dp: Dispatcher):
# errors
dp.register_errors_handler(errors.on_err_not_modified, exception=MessageNotModified)
dp.register_errors_handler(errors.on_err_station_not_exists, exception=StationNonExistsError)
dp.register_errors_handler(errors.on_err_api_not_responding, exception=ApiNotRespondingError)
dp.register_errors_handler(errors.on_err_api_timeout, exception=ApiNotRespondingError)
dp.register_errors_handler(errors.on_err_not_stations_found, exception=NoStopsError)
dp.register_errors_handler(errors.on_err_stop_already_saved, exception=StopAlreadySaved)
dp.register_errors_handler(errors.on_err_unknown_exception, exception=Exception) # Should be last in this list!

dp.register_message_handler(helpers.on_cancel, text_startswith=texts.cancel_button, state='*') # Should be first after error handlers!

# commands
dp.register_message_handler(commands.on_start_command, commands=['start'], state='*')
dp.register_message_handler(commands.on_help_command, commands=['help'])
dp.register_message_handler(commands.on_saved_stops_command, commands=['my_stops'])

# messages
dp.register_message_handler(
bus_handlers.on_stop_code,
lambda msg: msg.text.isdigit(),
content_types=ContentTypes.TEXT
)
dp.register_message_handler(bus_handlers.on_location, content_types=[ContentType.LOCATION, ContentType.VENUE])

# callbacks
dp.register_callback_query_handler(bus_handlers.on_terminate_call, text_startswith=CallbackPrefix.terminate_stop_updating)
dp.register_callback_query_handler(
bus_handlers.on_stop_call,
text_startswith=[CallbackPrefix.get_stop, CallbackPrefix.get_saved_stop]
)
dp.register_callback_query_handler(bus_handlers.on_save_stop, text_startswith=CallbackPrefix.save_stop)
dp.register_callback_query_handler(bus_handlers.on_remove_stop, text_startswith=CallbackPrefix.remove_stop)

# forms
dp.register_message_handler(forms.on_stop_rename, state=RenameStopState.waiting_for_stop_name)

# THIS ONE SHOULD BE LAST
dp.register_message_handler(helpers.incorrect_message_handler)
dp.error.register(errors.on_err_not_modified, ExceptionTypeFilter(TelegramBadRequest))
dp.error.register(errors.on_err_station_not_exists, ExceptionTypeFilter(StationNonExistsError))
dp.error.register(errors.on_err_api_not_responding, ExceptionTypeFilter(ApiNotRespondingError))
dp.error.register(errors.on_err_api_timeout, ExceptionTypeFilter(ApiNotRespondingError))
dp.error.register(errors.on_err_not_stations_found, ExceptionTypeFilter(NoStopsError))
dp.error.register(errors.on_err_stop_already_saved, ExceptionTypeFilter(StopAlreadySaved))
dp.error.register(errors.on_err_unknown_exception, ExceptionTypeFilter(Exception)) # Should be last in this list!

dp.include_router(cancel_router) # Should be first after error handlers!
dp.include_router(commands_router)
dp.include_router(bus_handlers_router)
dp.include_router(forms_router)
dp.include_router(incorrect_message_router)
Loading

0 comments on commit 89bbcb2

Please sign in to comment.