Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #2

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c939a6f
Initial commit: add client and server
s-klimov Jun 18, 2023
ea2472b
dictionary with busses fixed
s-klimov Jun 18, 2023
0bcab70
check msgType added
s-klimov Jun 18, 2023
cce0dc5
fixed create of buses list
s-klimov Jun 20, 2023
b4c66e8
server.py refactored
s-klimov Jun 22, 2023
a8a52fd
fake_bus.py refactored
s-klimov Jun 22, 2023
4530253
optimized message processing code
s-klimov Jun 22, 2023
f08312c
solution on web sockets
s-klimov Jun 25, 2023
d6934df
5 sockets used
s-klimov Jun 25, 2023
76cadc5
the cli is configured to simulate buses
s-klimov Jun 26, 2023
70e0de2
relaunch decorator added
s-klimov Jun 26, 2023
46469f2
warnings disabled in fake_bus.py
s-klimov Jun 28, 2023
e6898c7
listen_browser function added
s-klimov Jun 28, 2023
800a76d
top buses on the map added
s-klimov Jun 28, 2023
1519dc2
configured CLI output verbosity with logging
s-klimov Jun 29, 2023
b6013cf
attempt to transmit window coordinates
s-klimov Jun 29, 2023
b4d9812
attempt to transmit coordinates
s-klimov Jun 30, 2023
64321a4
added transmit coordinates
s-klimov Jun 30, 2023
7db6c64
class Bus added
s-klimov Jul 2, 2023
4935c32
class WindowBounds added
s-klimov Jul 2, 2023
ea41757
flake8 configuration added
s-klimov Jul 2, 2023
7d2d856
test initialized
s-klimov Jul 6, 2023
56ab7f7
Merge pull request #1 from s-klimov/feature/transmit_window_coordinates
s-klimov Jul 7, 2023
48fae58
harmful tests added
s-klimov Jul 9, 2023
8954520
tests for browser messages added
s-klimov Jul 10, 2023
fd9a5f1
refresh_timeout param added to server.py
s-klimov Jul 11, 2023
8f74f84
buses.gif added
s-klimov Jul 12, 2023
15d049a
documentations updated
s-klimov Jul 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[flake8]
max-line-length=119
max-doc-length=119
max-complexity=10
#extend=.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg
extend-exclude=venv,dist,conf,scripts,log_app,migrations
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@

<img src="screenshots/buses.gif">

## Требования к окружению
- python 3.10 и выше
- установленный пакет poetry для управления зависимостями
- браузер Google Chrome или др.

## Как запустить

- Скачайте код
- Откройте в браузере файл index.html
- Откройте в браузере файл index.html для запуска фронтенда
- Установите зависимости для бэкенда `poetry install`
- Запустите сервер `poetry run python server.py`
- Запустите при необходимости генератор фейковых автобусов `poetry run python fake_bus.py`

### Запустить модульные тесты
Запустите тесты, чтобы убедиться в работоспособности кода
```bash
poetry run python -m pytest
```


## Настройки
## Настройки фронтенда

Внизу справа на странице можно включить отладочный режим логгирования и указать нестандартный адрес веб-сокета.

Expand All @@ -20,7 +34,7 @@

Если что-то работает не так, как ожидалось, то начните с включения отладочного режима логгирования.

## Формат данных
## Формат данных для фронтенда

Фронтенд ожидает получить от сервера JSON сообщение со списком автобусов:

Expand Down Expand Up @@ -50,6 +64,22 @@
}
```

## Параметры скрипта сервера server.py

- `bus_port` - порт для имитатора автобусов
- `browser_port` - порт для браузера
- `refresh_timeout` — задержка в обновлении координат сервера
- `v` — настройка логирования

## Параметры скрипта имитации автобусов fake_bus.py

- `server` - адрес сервера
- `routes_number` — количество маршрутов
- `buses_per_route` — количество автобусов на каждом маршруте
- `websockets_number` — количество открытых веб-сокетов
- `emulator_id` — префикс к busId на случай запуска нескольких экземпляров имитатора
- `refresh_timeout` — пауза между отправками следующих координат фейковых автобусов.
- `v` — настройка уровня логирования


## Используемые библиотеки
Expand Down
257 changes: 257 additions & 0 deletions fake_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"""Скрипт имитации автобусов"""

import itertools
import json
import logging
import os
import warnings
from collections import deque
from contextlib import suppress, AsyncExitStack
from functools import wraps
from pathlib import Path
from random import randrange

import asyncclick as click
import asyncstdlib as a
import trio
import trio_websocket
from trio import MemoryReceiveChannel, MemorySendChannel
from trio import TrioDeprecationWarning
from trio_websocket import open_websocket_url

ROUTES_DIR = 'routes' # папка с маршрутами автобусов
BUS_NUM_LENGTH = 3 # количество символов в номере автобуса
RELAUNCH_INTERVAL = (
1 # интервал переподключения в секундах при обрыве соединения с сервером
)

warnings.filterwarnings(action='ignore', category=TrioDeprecationWarning)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%m/%d/%Y %H:%M:%S',
)
logger = logging.getLogger('fake-bus')


async def load_routes(directory_path=ROUTES_DIR):
"""
Генератор, который возвращает json'ы с маршрутами из файлов папки с маршрутами.
:param directory_path: Имя/путь папки с json-файлами, содержащими маршруты.
"""
for filename in os.listdir(directory_path):
logger.info('Открываем файл %s' % (filename,))
if filename.endswith('.json'):
filepath = os.path.join(Path(directory_path), filename)
async with await trio.open_file(
filepath, 'r', encoding='utf8'
) as afp:
route_full_info = await afp.read()
yield json.loads(route_full_info)


async def run_bus(
send_channel: MemorySendChannel,
bus_id: str,
points: list,
route_name: str,
refresh_timeout: float,
/,
):
"""
Хэндлер запуска автобуса в очередь trio.
:param send_channel: Очередь trio.
:param bus_id: Номер автобуса.
:param points: Точки маршрута.
:param route_name: Номер маршрута.
:param refresh_timeout: Интервал в секундах между перемещениями автобусов по точкам маршрутов на карте.
"""
for lat, long in itertools.cycle(points):
coordinate = {
'busId': bus_id,
'lat': lat,
'lng': long,
'route': route_name,
}
await send_channel.send(json.dumps(coordinate))
await trio.sleep(refresh_timeout)


def generate_bus_id(route_id, bus_index, emulator_id):
"""Генератор номера автобуса."""
return f'{route_id}-{emulator_id}{str(bus_index).zfill(BUS_NUM_LENGTH)}'


def relaunch_on_disconnect(f):
"""
Декоратор, отслеживающий соединение с сервером.
При обнаружении разрыва соединения происходит попытка нового подключения.
"""

@wraps(f)
async def wrapper(*args, **kwds):
with suppress(KeyboardInterrupt):
while True:
try:
await f(*args, **kwds)
except (
trio_websocket.HandshakeError,
trio_websocket.ConnectionClosed,
):
logger.error(
'Ошибка соединения с сервером. Попытка подключения через %d сек'
% (RELAUNCH_INTERVAL,)
)
await trio.sleep(RELAUNCH_INTERVAL)

return wrapper


@relaunch_on_disconnect
async def send_updates(
server: str,
websockets_number: int,
receive_channel: MemoryReceiveChannel,
/,
):
"""
Отправляет координаты автобуса по web-сокету. Web-сокет выбирается случайным образом из заданных.
:param server: Адрес сервера.
:param websockets_number: Количество открытых web-сокетов.
:param receive_channel: Канал для приема координат автобуса для последующей отправки.
"""
async with AsyncExitStack() as stack:
sockets = [
await stack.enter_async_context(open_websocket_url(server))
for _ in range(websockets_number)
]
logger.info('Открыто %d сокетов.' % (len(sockets),))
async for message in receive_channel:
with suppress(KeyboardInterrupt):
await sockets[randrange(websockets_number)].send_message(
message
)


def validate_routes_number(ctx, param, value) -> int:
"""Валидатор для параметра routes_number. Ограничен количеством файлов с маршрутами в папке routes."""
if value < 1 or value > 595:
raise click.BadParameter(
'Количество маршрутов должно быть от 1 до 595.'
)
return value


def get_log_level(ctx, param, value) -> int:
"""Преобразует количество указанных v (verbose) в параметрах скрипта к уровню логирования"""
levels = [
logging.ERROR,
logging.WARNING,
logging.INFO,
logging.DEBUG,
]
level = levels[min(value, len(levels) - 1)]

return level


# !!! Просьба не менять входные аргументы
@click.command()
@click.option(
'--server',
default='ws://127.0.0.1:8080/ws',
show_default=True,
help='Адрес сервера.',
)
@click.option(
'--routes_number',
default=595,
show_default=True,
callback=validate_routes_number,
help='Количество маршрутов (от 1 до 595).',
)
@click.option(
'--buses_per_route',
default=100,
show_default=True,
help='Количество автобусов на каждом маршруте.',
)
@click.option(
'--websockets_number',
default=10,
show_default=True,
help='Количество открытых веб-сокетов.',
)
@click.option(
'--emulator_id',
default='',
help='Префикс к busId на случай запуска нескольких экземпляров имитатора.',
)
@click.option(
'--refresh_timeout',
type=float,
default=0.3,
show_default=True,
help='Пауза между отправками следующих координат фейковых автобусов.',
)
@click.option(
'-v',
'--verbose',
count=True,
callback=get_log_level,
help='Настройка логирования.',
) # https://click.palletsprojects.com/en/8.1.x/options/#counting
async def main(
server,
routes_number,
buses_per_route,
websockets_number,
emulator_id,
refresh_timeout,
verbose,
):

logger.setLevel(verbose)

send_channel, receive_channel = trio.open_memory_channel(0)

async with trio.open_nursery() as nursery:
nursery.start_soon(
send_updates, server, websockets_number, receive_channel
)

async for i, route in a.enumerate(load_routes()):
if i == routes_number:
break

coordinates = (
route['coordinates'] + route['coordinates'][::-1]
) # добавляем обратный путь

points = deque(coordinates)

for bus_index in range(randrange(1, buses_per_route)):
logger.debug(
'Запускаем автобус %s по маршруту %s'
% (
bus_index,
route['name'],
)
)

points.rotate(
randrange(len(coordinates))
) # поездку начинаем с произвольной точки маршрута

nursery.start_soon(
run_bus,
send_channel,
generate_bus_id(route['name'], bus_index, emulator_id),
points.copy(),
route['name'],
refresh_timeout,
)


if __name__ == '__main__':
with suppress(KeyboardInterrupt):
trio.run(main(_anyio_backend='trio'))
Loading