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

chore(refactoring): split to class-based version #22

Merged
merged 5 commits into from
Sep 12, 2022

Conversation

mmev
Copy link
Contributor

@mmev mmev commented Jun 13, 2022

Начал рефакторить, за пару часов успел сделать следующее:

  • Написать абстрактные классы для всех типов хэндлеров
  • Вытащить все хендлеры в отдельные классы
  • Разбил существующие формы на классы
  • Написал базовый класс для моделей базы данных, но не успел заменить на её использование

Оттестированы функции бота, однако уведомления об изменениях не проверены.
Реквест получился большой, лучше спуллить локально и там смотреть изменения.

@mmev
Copy link
Contributor Author

mmev commented Jun 13, 2022

Из предложений по улучшению:

  • Разбить весь спагетти код на классы, вызывать их при необходимости
  • Вытащить запросы к ориоксу в Celery + очередь

@llirrikk
Copy link
Member

Спасибо большое, посмотрю на следующей неделе. Как раз в планах было отрефакторить

@mmev
Copy link
Contributor Author

mmev commented Jun 14, 2022

Сделал еще кусок рефакторинга:

  • Вытащил ассетсы в отдельную директорую, удалив прошлую корневую
  • Написал небольшой хелпер для взаимодействия с ассестами
  • Переместил остатки хелперов в директорию приложения
  • Чутка упростил хелпер для генерации картинок (с неймингом было сложно)
  • Вытащил переменные в environment файл, сразу сделал template, для генерации через GitHub Actions
  • Для удобной работы с env файлами установлена библиотека python-dotenv
  • Подключена SQLAlchemy для работы с базой данных, пока используется встроенный SQLite движок
  • Подключен Alembic для миграций схемы, сделана первая миграция с тремя таблицами
  • Написан модуль для фикстур, который позволяет вставлять стандартные значения в базу данных, реализовано для стандартного значения AdminStatistics
  • Выпилены первые 2 SQL файла: создание AdminStatistics (заменено на Alembic migration) и заполнение его данными (заменено более гибкими фикстурами)
  • Обновлён список зависимостей

Вроде ничего не успустил, думаю к выходным добью рефакторинг...

Еще думаю на счет миграции уже существующих данных пользователей, пока в голову приходит какой-то скрипт, который перетащит данные в нужные места, однако не уверен, что это хорошая затея. @llirrikk что думаешь об этом? Сколько вообще пользователей в продакшене? Насколько сложно будет руками перенести?

@llirrikk
Copy link
Member

С SQLAlchemy и Alembic не работал, но вот, кажется, пришло их время. Они не могут как django migrations сделать автоматически новую таблицу из существующей в БД?

скрипт, который перетащит данные в нужные места

нужные места - это новая таблица в БД? Чем она отличается от старой? Только названиями таблиц и колонок?

На данный момент у нас 665 пользователей, из которых 520 активных, поэтому если руками (скриптом), то придется останавливать продакшн на время этих миграций, чтобы пользователи не могли вносить изменения в это время

@mmev
Copy link
Contributor Author

mmev commented Jun 15, 2022

С SQLAlchemy и Alembic не работал, но вот, кажется, пришло их время. Они не могут как django migrations сделать автоматически новую таблицу из существующей в БД?

Там выходит такая история, отталкиваясь от SQL файлов я написал модели app/models/admin/AdminStatistics.py, app/models/users/UserStatus.py, app/models/users/UserNotifySettings.py и выполнил команду, после чего Alembic сгенерировал файл миграции в котором происходит создание этих моделей - app/migrations/version/e09d97658b84_initial_migration.py. В дальшейнем, если нужно внести какие-то изменения в БД, то они делаются именно в моделях и после чего запуском скрипта генерируется миграция, которая так-же скриптом применяется.

А вот с дата миграциями все немного сложнее, это придется делать скриптом, тк дата тайпы отличаются и SQLAlchemy не сможет работать с прошлым файлом базы.

Я больше переживаю о файлах пользователей, в которых хранятся данные, я планирую их переместить в другое месте, конкретнее app/storage или просто storage. В целом заменить все методы работы с файлами на централизованные, по этому нужно будет потом подумать, как корректно мигрировать именно userdata.

Давай созвонимся сегодня в telegram

@mmev
Copy link
Contributor Author

mmev commented Jun 15, 2022

Команда для создания новой миграции с автоматической проверкой изменений:

alembic revision --autogenerate -m "Initial migration"

Команда что бы применить изменения:

alembic migrate

Copy link
Member

@llirrikk llirrikk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍, но pep8 не везде соблюден

Comment on lines +17 to +21
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine(config.DATABASE_URL, convert_unicode=True)
return scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

может логику инициализации БД вынести в db/__init__.py?

Comment on lines +28 to +37
def insert_data(self) -> bool:
if not self.need_to_add_values():
return False

values = self.values() if callable(self.values) else self.values

for value in values:
model = self.model()
getattr(model, self.fill_method_name)(**value)
model.save()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Возвращаемый bool нигде далее не обрабатывается. Возможно, надо заменить на None, ну или raise что-то. К тому же из функции может вовсе ничего не возвращаться, что противоречит аннотации

Comment on lines +58 to +65
if text is not None:
dispatcher_.register_message_handler(handler_class.process, text=text, state=state)

if commands is not None:
dispatcher_.register_message_handler(handler_class.process, commands=commands, state=state)

if text is None and commands is None and state is not None:
dispatcher_.register_message_handler(handler_class.process, state=state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В 3.10 появился match/case, может, его сюда засунуть?


import db.admins_statistics
from app.handlers.commands.settings import NotificationSettingsCommandHandler
from config import Config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Почему тут Config, если везде config?

from app.helpers import OrioksHelper, TelegramMessageHelper
from app.menus.orioks import OrioksAuthFailedMenu
from app.menus.start import StartMenu
from config import Config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

То же самое?


__tablename__ = 'user_status'

user_telegram_id = Column(Integer, nullable=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Наверное лучше прописать связь с таблицей user_notify_settings как One To One

class BaseModel(DeclarativeModelBase):
__abstract__ = True

id = Column(Integer, primary_key=True)
Copy link
Member

@llirrikk llirrikk Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Почему pk именно на id? Думаю лучше ставить на user_telegram_id ну и убрать id

Comment on lines +11 to +12
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А это зачем?

Comment on lines +10 to +12
scheduled_requests = Column(Integer, nullable=False, default=0)
success_logins = Column(Integer, nullable=False, default=0)
failed_logins = Column(Integer, nullable=False, default=0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тип лучше установить на BigInteger. Еще лучше сделать проверку на неотрицательность

@@ -0,0 +1 @@
from .StartMenu import StartMenu
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А где магический __all__, как и везде в других местах?

@llirrikk
Copy link
Member

  • В app/fixtures/AbstractFixture.py добавил возвращение True в конце
  • Заменил аннотации типов List, Dict на list, dict соответственно (python 3.9)
  • Заменил импорт Config на config для идентичности
  • Добавил абстрактные классы для AbstractInlineKeyboard, AbstractReplyKeyboard клавиатур двух типов

@llirrikk llirrikk merged commit f9f248c into orioks-monitoring:main Sep 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants