diff --git a/.circleci/config.yml b/.circleci/config.yml index 6540a1b..16afdc4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ jobs: python3 -m venv venv source ./venv/bin/activate pip install -U pip setuptools tox - tox -epy38,flake8_ci + tox -e py38,lint_ci - save_cache: paths: - ./venv diff --git a/ChangeLog.rst b/ChangeLog.rst index 6972a3b..2778a2d 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -5,7 +5,7 @@ Unreleased Release Notes - 2020-11-02 -------------------------- - [#213] リリースノートの最新バージョンの記述を返すversionコマンドを追加 - +- [#215] blackの導入 Release Notes - 2020-10-30 -------------------------- diff --git a/src/alembic/migrations/env.py b/src/alembic/migrations/env.py index f0e1aee..aba7c2a 100644 --- a/src/alembic/migrations/env.py +++ b/src/alembic/migrations/env.py @@ -17,7 +17,7 @@ # Baseをimportするのでharoのrootパスを追加 # NOTE: run.py と統合できない? -root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) sys.path.append(root) # this is the Alembic Config object, which provides @@ -26,8 +26,9 @@ # ini ファイルに環境変数を渡すことができないため、ここで追加 from slackbot_settings import SQLALCHEMY_URL, SQLALCHEMY_ECHO -config.set_main_option('sqlalchemy.url', SQLALCHEMY_URL) -config.set_main_option('sqlalchemy.echo', SQLALCHEMY_ECHO) + +config.set_main_option("sqlalchemy.url", SQLALCHEMY_URL) +config.set_main_option("sqlalchemy.echo", SQLALCHEMY_ECHO) # Interpret the config file for Python logging. # This line sets up loggers basically. @@ -64,8 +65,7 @@ def run_migrations_offline(): """ url = config.get_main_section["sqlalchemy.url"] - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True) + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -80,18 +80,17 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix='sqlalchemy.', - poolclass=pool.NullPool) + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() + if context.is_offline_mode(): run_migrations_offline() else: diff --git a/src/alembic/migrations/versions/8e7a2acd24b8_add_resource_model.py b/src/alembic/migrations/versions/8e7a2acd24b8_add_resource_model.py index 1cf1a17..b599159 100644 --- a/src/alembic/migrations/versions/8e7a2acd24b8_add_resource_model.py +++ b/src/alembic/migrations/versions/8e7a2acd24b8_add_resource_model.py @@ -10,25 +10,27 @@ # revision identifiers, used by Alembic. -revision = '8e7a2acd24b8' -down_revision = '919388975e00' +revision = "8e7a2acd24b8" +down_revision = "919388975e00" branch_labels = None depends_on = None + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('resource', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('channel_id', sa.Unicode(length=249), nullable=False), - sa.Column('name', sa.Unicode(length=249), nullable=False), - sa.Column('status', sa.Unicode(length=249), nullable=True), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "resource", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("channel_id", sa.Unicode(length=249), nullable=False), + sa.Column("name", sa.Unicode(length=249), nullable=False), + sa.Column("status", sa.Unicode(length=249), nullable=True), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('resource') + op.drop_table("resource") # ### end Alembic commands ### diff --git a/src/alembic/migrations/versions/919388975e00_initialize.py b/src/alembic/migrations/versions/919388975e00_initialize.py index a42a797..e1de86d 100644 --- a/src/alembic/migrations/versions/919388975e00_initialize.py +++ b/src/alembic/migrations/versions/919388975e00_initialize.py @@ -10,113 +10,130 @@ # revision identifiers, used by Alembic. -revision = '919388975e00' +revision = "919388975e00" down_revision = None branch_labels = None depends_on = None + def upgrade(): ### commands auto generated by Alembic - please adjust! ### - op.create_table('cleaning', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('slack_id', sa.Unicode(length=11), nullable=False), - sa.Column('day_of_week', sa.Integer(), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "cleaning", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("slack_id", sa.Unicode(length=11), nullable=False), + sa.Column("day_of_week", sa.Integer(), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('create_command', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=100), nullable=False), - sa.Column('creator', sa.Unicode(length=100), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + op.create_table( + "create_command", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=100), nullable=False), + sa.Column("creator", sa.Unicode(length=100), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) - op.create_table('kintai_history', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Unicode(length=100), nullable=False), - sa.Column('is_workon', sa.Boolean(), nullable=True), - sa.Column('registered_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "kintai_history", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Unicode(length=100), nullable=False), + sa.Column("is_workon", sa.Boolean(), nullable=True), + sa.Column("registered_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('kudo_history', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=100), nullable=False), - sa.Column('from_user_id', sa.Unicode(length=100), nullable=False), - sa.Column('delta', sa.Integer(), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "kudo_history", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=100), nullable=False), + sa.Column("from_user_id", sa.Unicode(length=100), nullable=False), + sa.Column("delta", sa.Integer(), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('redbull_history', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Unicode(length=100), nullable=False), - sa.Column('delta', sa.Integer(), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "redbull_history", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Unicode(length=100), nullable=False), + sa.Column("delta", sa.Integer(), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('redmine_projectchannel', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('project_id', sa.Integer(), nullable=True), - sa.Column('channels', sa.Unicode(length=249), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('project_id') + op.create_table( + "redmine_projectchannel", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=True), + sa.Column("channels", sa.Unicode(length=249), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("project_id"), ) - op.create_table('redmine_users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Unicode(length=9), nullable=False), - sa.Column('api_key', sa.Unicode(length=40), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('api_key'), - sa.UniqueConstraint('user_id') + op.create_table( + "redmine_users", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Unicode(length=9), nullable=False), + sa.Column("api_key", sa.Unicode(length=40), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("api_key"), + sa.UniqueConstraint("user_id"), ) - op.create_table('thx_history', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Unicode(length=100), nullable=False), - sa.Column('from_user_id', sa.Unicode(length=100), nullable=False), - sa.Column('word', sa.Unicode(length=1024), nullable=False), - sa.Column('channel_id', sa.Unicode(length=100), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "thx_history", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Unicode(length=100), nullable=False), + sa.Column("from_user_id", sa.Unicode(length=100), nullable=False), + sa.Column("word", sa.Unicode(length=1024), nullable=False), + sa.Column("channel_id", sa.Unicode(length=100), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('user_alias_name', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('slack_id', sa.Unicode(length=100), nullable=False), - sa.Column('alias_name', sa.Unicode(length=100), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('alias_name') + op.create_table( + "user_alias_name", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("slack_id", sa.Unicode(length=100), nullable=False), + sa.Column("alias_name", sa.Unicode(length=100), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("alias_name"), ) - op.create_table('water_history', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Unicode(length=100), nullable=False), - sa.Column('delta', sa.Integer(), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "water_history", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Unicode(length=100), nullable=False), + sa.Column("delta", sa.Integer(), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('term', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('create_command', sa.Integer(), nullable=True), - sa.Column('word', sa.Unicode(length=1024), nullable=False), - sa.Column('creator', sa.Unicode(length=100), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['create_command'], ['create_command.id'], onupdate='CASCADE', ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('word') + op.create_table( + "term", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("create_command", sa.Integer(), nullable=True), + sa.Column("word", sa.Unicode(length=1024), nullable=False), + sa.Column("creator", sa.Unicode(length=100), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["create_command"], + ["create_command.id"], + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("word"), ) ### end Alembic commands ### def downgrade(): ### commands auto generated by Alembic - please adjust! ### - op.drop_table('term') - op.drop_table('water_history') - op.drop_table('user_alias_name') - op.drop_table('thx_history') - op.drop_table('redmine_users') - op.drop_table('redmine_projectchannel') - op.drop_table('redbull_history') - op.drop_table('kudo_history') - op.drop_table('kintai_history') - op.drop_table('create_command') - op.drop_table('cleaning') + op.drop_table("term") + op.drop_table("water_history") + op.drop_table("user_alias_name") + op.drop_table("thx_history") + op.drop_table("redmine_users") + op.drop_table("redmine_projectchannel") + op.drop_table("redbull_history") + op.drop_table("kudo_history") + op.drop_table("kintai_history") + op.drop_table("create_command") + op.drop_table("cleaning") ### end Alembic commands ### diff --git a/src/alembic/migrations/versions/98ecbc1e5b66_add_emergency_models.py b/src/alembic/migrations/versions/98ecbc1e5b66_add_emergency_models.py index 9049b40..df224f2 100644 --- a/src/alembic/migrations/versions/98ecbc1e5b66_add_emergency_models.py +++ b/src/alembic/migrations/versions/98ecbc1e5b66_add_emergency_models.py @@ -10,38 +10,44 @@ # revision identifiers, used by Alembic. -revision = '98ecbc1e5b66' -down_revision = '8e7a2acd24b8' +revision = "98ecbc1e5b66" +down_revision = "8e7a2acd24b8" branch_labels = None depends_on = None + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('emergency_timeline', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('created_by', sa.Unicode(length=9), nullable=False), - sa.Column('room', sa.Unicode(length=9), nullable=False), - sa.Column('title', sa.Unicode(length=128), nullable=False), - sa.Column('is_closed', sa.BOOLEAN(), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.Column('utime', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "emergency_timeline", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_by", sa.Unicode(length=9), nullable=False), + sa.Column("room", sa.Unicode(length=9), nullable=False), + sa.Column("title", sa.Unicode(length=128), nullable=False), + sa.Column("is_closed", sa.BOOLEAN(), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.Column("utime", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('timeline_log', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('timeline_id', sa.Integer(), nullable=False), - sa.Column('created_by', sa.Unicode(length=9), nullable=False), - sa.Column('ctime', sa.DateTime(), nullable=False), - sa.Column('utime', sa.DateTime(), nullable=False), - sa.Column('entry', sa.Text(), nullable=False), - sa.ForeignKeyConstraint(['timeline_id'], ['emergency_timeline.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "timeline_log", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("timeline_id", sa.Integer(), nullable=False), + sa.Column("created_by", sa.Unicode(length=9), nullable=False), + sa.Column("ctime", sa.DateTime(), nullable=False), + sa.Column("utime", sa.DateTime(), nullable=False), + sa.Column("entry", sa.Text(), nullable=False), + sa.ForeignKeyConstraint( + ["timeline_id"], + ["emergency_timeline.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('timeline_log') - op.drop_table('emergency_timeline') + op.drop_table("timeline_log") + op.drop_table("emergency_timeline") # ### end Alembic commands ### diff --git a/src/db.py b/src/db.py index 13d0b9f..1223ff5 100644 --- a/src/db.py +++ b/src/db.py @@ -21,7 +21,7 @@ from haro.plugins.resource_models import Resource # noqa -def init_dbsession(config, prefix='sqlalchemy.'): +def init_dbsession(config, prefix="sqlalchemy."): """configに設定した値でDBの設定情報を初期化 :param dict config: `alembic/conf.ini` から生成したdictの設定値 diff --git a/src/haro/alias.py b/src/haro/alias.py index afa28bb..2b244b4 100644 --- a/src/haro/alias.py +++ b/src/haro/alias.py @@ -14,7 +14,9 @@ def get_slack_id(session, user_name): """ slack_id = get_slack_id_by_name(user_name) if not slack_id: - slack_id = (session.query(UserAliasName.slack_id) - .filter(UserAliasName.alias_name == user_name) - .scalar()) + slack_id = ( + session.query(UserAliasName.slack_id) + .filter(UserAliasName.alias_name == user_name) + .scalar() + ) return slack_id diff --git a/src/haro/arg_validator.py b/src/haro/arg_validator.py index 761ffd7..c90fa7a 100644 --- a/src/haro/arg_validator.py +++ b/src/haro/arg_validator.py @@ -76,7 +76,7 @@ def is_valid(self): continue # clean_xxx メソッドがある場合は呼び出して検証 - f = getattr(self, 'clean_{}'.format(name), None) + f = getattr(self, "clean_{}".format(name), None) if not f or not callable(f): self.cleaned_data[name] = given logger.debug("do not have clean_%s method", name) @@ -89,7 +89,7 @@ def is_valid(self): # 追加フィールドの検証 for extra_name in self.extras: - f = getattr(self, 'clean_{}'.format(extra_name)) + f = getattr(self, "clean_{}".format(extra_name)) try: self.cleaned_data[extra_name] = f() except ValidationError as e: diff --git a/src/haro/botmessage.py b/src/haro/botmessage.py index fe762b8..cbb29d2 100644 --- a/src/haro/botmessage.py +++ b/src/haro/botmessage.py @@ -5,7 +5,7 @@ def botsend(message, text): :param message: slackbotのmessageオブジェクト :param text: 送信するテキストメッセージ """ - if 'thread_ts' in message.body: + if "thread_ts" in message.body: # スレッド内のメッセージの場合 message.send(text, thread_ts=message.thread_ts) else: @@ -20,7 +20,7 @@ def botreply(message, text): :param message: slackbotのmessageオブジェクト :param text: 送信するテキストメッセージ """ - if 'thread_ts' in message.body: + if "thread_ts" in message.body: # スレッド内のメッセージの場合 message.reply(text=text, in_thread=True) else: @@ -36,9 +36,14 @@ def webapisend(message, text): """ sc = message._client.webapi channel = message.channel - channel_id = channel._body['id'] - if 'thread_ts' in message.body: - sc.chat.post_message(channel_id, text=text, as_user=True, - unfurl_links=True, thread_ts=message.thread_ts) + channel_id = channel._body["id"] + if "thread_ts" in message.body: + sc.chat.post_message( + channel_id, + text=text, + as_user=True, + unfurl_links=True, + thread_ts=message.thread_ts, + ) else: sc.chat.post_message(channel_id, text=text, as_user=True, unfurl_links=True) diff --git a/src/haro/plugins/alias.py b/src/haro/plugins/alias.py index 11960d8..9e22763 100644 --- a/src/haro/plugins/alias.py +++ b/src/haro/plugins/alias.py @@ -14,8 +14,8 @@ """ -@respond_to(r'^alias\s+show$') -@respond_to(r'^alias\s+show\s+(\S+)$') +@respond_to(r"^alias\s+show$") +@respond_to(r"^alias\s+show\s+(\S+)$") def show_user_alias_name(message, user_name=None): """ユーザーのエイリアス名一覧を表示する @@ -25,26 +25,27 @@ def show_user_alias_name(message, user_name=None): if user_name: slack_id = get_slack_id_by_name(user_name) else: - slack_id = message.body['user'] + slack_id = message.body["user"] user_name = get_user_name(slack_id) if not slack_id: - botsend(message, '{}に紐づくSlackのuser_idは存在しません'.format(user_name)) + botsend(message, "{}に紐づくSlackのuser_idは存在しません".format(user_name)) return s = Session() - alias_names = [user.alias_name for user in - s.query(UserAliasName) - .filter(UserAliasName.slack_id == slack_id)] + alias_names = [ + user.alias_name + for user in s.query(UserAliasName).filter(UserAliasName.slack_id == slack_id) + ] - pt = PrettyTable(['ユーザー名', 'Slack ID', 'エイリアス名']) - alias_name = ','.join(alias_names) + pt = PrettyTable(["ユーザー名", "Slack ID", "エイリアス名"]) + alias_name = ",".join(alias_names) pt.add_row([user_name, slack_id, alias_name]) - botsend(message, '```{}```'.format(pt)) + botsend(message, "```{}```".format(pt)) -@respond_to(r'^alias\s+add\s+(\S+)$') -@respond_to(r'^alias\s+add\s+(\S+)\s+(\S+)$') +@respond_to(r"^alias\s+add\s+(\S+)$") +@respond_to(r"^alias\s+add\s+(\S+)\s+(\S+)$") def alias_name(message, user_name, alias_name=None): """指定したユーザにエイリアス名を紐付ける @@ -60,32 +61,33 @@ def alias_name(message, user_name, alias_name=None): else: # 投稿者のエイリアス名を更新するパターン alias_name = user_name - slack_id = message.body['user'] + slack_id = message.body["user"] user_name = get_user_name(slack_id) user = get_slack_id_by_name(alias_name) if user: - botsend(message, '`{}` はユーザーが存在しているので使用できません'.format(alias_name)) + botsend(message, "`{}` はユーザーが存在しているので使用できません".format(alias_name)) return if not slack_id: - botsend(message, '{}に紐づくSlackのuser_idは存在しません'.format(user_name)) + botsend(message, "{}に紐づくSlackのuser_idは存在しません".format(user_name)) return s = Session() - alias_user_name = (s.query(UserAliasName) - .filter(UserAliasName.alias_name == alias_name)) + alias_user_name = s.query(UserAliasName).filter( + UserAliasName.alias_name == alias_name + ) if s.query(alias_user_name.exists()).scalar(): - botsend(message, 'エイリアス名 `{}` は既に登録されています'.format(alias_name)) + botsend(message, "エイリアス名 `{}` は既に登録されています".format(alias_name)) return s.add(UserAliasName(slack_id=slack_id, alias_name=alias_name)) s.commit() - botsend(message, '{}のエイリアス名に `{}` を追加しました'.format(user_name, alias_name)) + botsend(message, "{}のエイリアス名に `{}` を追加しました".format(user_name, alias_name)) -@respond_to(r'^alias\s+del\s+(\S+)$') -@respond_to(r'^alias\s+del\s+(\S+)\s+(\S+)$') +@respond_to(r"^alias\s+del\s+(\S+)$") +@respond_to(r"^alias\s+del\s+(\S+)\s+(\S+)$") def unalias_name(message, user_name, alias_name=None): """ユーザーに紐づくエイリアス名を削除する @@ -101,28 +103,30 @@ def unalias_name(message, user_name, alias_name=None): else: # 投稿者のエイリアス名を更新するパターン alias_name = user_name - slack_id = message.body['user'] + slack_id = message.body["user"] user_name = get_user_name(slack_id) if not slack_id: - botsend(message, '{}に紐づくSlackのuser_idは存在しません'.format(user_name)) + botsend(message, "{}に紐づくSlackのuser_idは存在しません".format(user_name)) return s = Session() - alias_user_name = (s.query(UserAliasName) - .filter(UserAliasName.slack_id == slack_id) - .filter(UserAliasName.alias_name == alias_name) - .one_or_none()) + alias_user_name = ( + s.query(UserAliasName) + .filter(UserAliasName.slack_id == slack_id) + .filter(UserAliasName.alias_name == alias_name) + .one_or_none() + ) if alias_user_name: s.delete(alias_user_name) s.commit() - botsend(message, '{}のエイリアス名から `{}` を削除しました'.format(user_name, alias_name)) + botsend(message, "{}のエイリアス名から `{}` を削除しました".format(user_name, alias_name)) else: - botsend(message, '{}のエイリアス名 `{}` は登録されていません'.format(user_name, alias_name)) + botsend(message, "{}のエイリアス名 `{}` は登録されていません".format(user_name, alias_name)) -@respond_to(r'^alias\s+help$') +@respond_to(r"^alias\s+help$") def show_help_alias_commands(message): """Userコマンドのhelpを表示 diff --git a/src/haro/plugins/alias_models.py b/src/haro/plugins/alias_models.py index a904e30..11414ad 100644 --- a/src/haro/plugins/alias_models.py +++ b/src/haro/plugins/alias_models.py @@ -5,9 +5,9 @@ class UserAliasName(Base): - """Slackのuser_idに紐づく名前を管理するモデル - """ - __tablename__ = 'user_alias_name' + """Slackのuser_idに紐づく名前を管理するモデル""" + + __tablename__ = "user_alias_name" id = Column(Integer, primary_key=True) slack_id = Column(Unicode(100), nullable=False) diff --git a/src/haro/plugins/amesh.py b/src/haro/plugins/amesh.py index 113970e..4cb2c6b 100644 --- a/src/haro/plugins/amesh.py +++ b/src/haro/plugins/amesh.py @@ -49,7 +49,9 @@ def amesh(message): try: # 画像の合成 # 000 はエリアごとの固定値で050,100,150があるけど決め打ちで - with _get_image("http://tokyo-ame.jwa.or.jp/map/msk000.png") as image_msk, _get_image( + with _get_image( + "http://tokyo-ame.jwa.or.jp/map/msk000.png" + ) as image_msk, _get_image( "http://tokyo-ame.jwa.or.jp/map/map000.jpg" ) as image_map, _get_image( "http://tokyo-ame.jwa.or.jp/mesh/000/{}{}.gif".format(yyyymmddhh, mm) @@ -64,8 +66,10 @@ def amesh(message): merged2.save(tmpname) # せっかくなので天気もみれるようにしてる - comment = "時刻: {:%Y年%m月%d日 %H}:{}\n".format(n, mm) + \ - "公式: http://tokyo-ame.jwa.or.jp/\n" + comment = ( + "時刻: {:%Y年%m月%d日 %H}:{}\n".format(n, mm) + + "公式: http://tokyo-ame.jwa.or.jp/\n" + ) # 外部サイトに投稿してURLを貼る方法(S3とか)だとaccesskey設定等いるのでslackに直接アップロード sc = message._client.webapi diff --git a/src/haro/plugins/cleaning.py b/src/haro/plugins/cleaning.py index c31d01f..586fb26 100644 --- a/src/haro/plugins/cleaning.py +++ b/src/haro/plugins/cleaning.py @@ -22,25 +22,25 @@ """ CLEANING_TASKS = [ - 'ゴミ集め(シュレッダー) ', - 'ゴミ出し 火曜・金曜', - '机拭き: bar, showroom, 窓際, おやつ, スタンディング', - 'フリーアドレス席の汚れている机拭き', - 'barのディスプレイから出てるケーブルを後ろ側にある取っ手にかける', - '空気清浄機のフル稼働(執務室,bar,showroom)', - '執務室の2台の加湿器の注水(冬場のみ)&フル稼働(消し忘れ防止のためにタイマーで設定しましょう) ', - '会議室の加湿器の注水(冬場のみ)&フル稼働', + "ゴミ集め(シュレッダー) ", + "ゴミ出し 火曜・金曜", + "机拭き: bar, showroom, 窓際, おやつ, スタンディング", + "フリーアドレス席の汚れている机拭き", + "barのディスプレイから出てるケーブルを後ろ側にある取っ手にかける", + "空気清浄機のフル稼働(執務室,bar,showroom)", + "執務室の2台の加湿器の注水(冬場のみ)&フル稼働(消し忘れ防止のためにタイマーで設定しましょう) ", + "会議室の加湿器の注水(冬場のみ)&フル稼働", ] # 掃除作業を表示用に整形した文字列 -FORMATTED_CLEANING_TASKS = ( - '掃除でやることリスト\n' + '\n'.join(['- [] {}'.format(row) for row in CLEANING_TASKS]) +FORMATTED_CLEANING_TASKS = "掃除でやることリスト\n" + "\n".join( + ["- [] {}".format(row) for row in CLEANING_TASKS] ) -DAY_OF_WEEK = '月火水木金' +DAY_OF_WEEK = "月火水木金" -@respond_to(r'^cleaning\s+help$') +@respond_to(r"^cleaning\s+help$") def show_help_cleaning_commands(message): """Cleaningコマンドのhelpを表示 @@ -49,7 +49,7 @@ def show_help_cleaning_commands(message): botsend(message, HELP) -@respond_to(r'^cleaning\s+task$') +@respond_to(r"^cleaning\s+task$") def show_cleaning_task(message): """掃除作業一覧を表示 @@ -58,7 +58,7 @@ def show_cleaning_task(message): botsend(message, FORMATTED_CLEANING_TASKS) -@respond_to(r'^cleaning\s+list$') +@respond_to(r"^cleaning\s+list$") def show_cleaning_list(message): """掃除当番の一覧を表示する @@ -71,16 +71,16 @@ def show_cleaning_list(message): user = get_user_display_name(c.slack_id) dow2users.setdefault(c.day_of_week, []).append(user) - pt = PrettyTable(['曜日', '掃除当番']) - pt.align['掃除当番'] = 'l' + pt = PrettyTable(["曜日", "掃除当番"]) + pt.align["掃除当番"] = "l" for day_of_week, users in dow2users.items(): dow = DAY_OF_WEEK[day_of_week] - str_users = ', '.join(users) + str_users = ", ".join(users) pt.add_row([dow, str_users]) - botsend(message, '```{}```'.format(pt)) + botsend(message, "```{}```".format(pt)) -@respond_to(r'^cleaning\s+today$') +@respond_to(r"^cleaning\s+today$") def show_today_cleaning_list(message): """今日の掃除当番を表示する @@ -89,12 +89,14 @@ def show_today_cleaning_list(message): dow = datetime.datetime.today().weekday() s = Session() - users = [get_user_display_name(c.slack_id) for - c in s.query(Cleaning).filter(Cleaning.day_of_week == dow)] - botsend(message, '今日の掃除当番は{}です'.format('、'.join(users))) + users = [ + get_user_display_name(c.slack_id) + for c in s.query(Cleaning).filter(Cleaning.day_of_week == dow) + ] + botsend(message, "今日の掃除当番は{}です".format("、".join(users))) -@respond_to(r'^cleaning\s+add\s+(\S+)\s+(\S+)$') +@respond_to(r"^cleaning\s+add\s+(\S+)\s+(\S+)$") def cleaning_add(message, user_name, day_of_week): """指定した曜日の掃除当番にユーザーを追加する @@ -103,26 +105,26 @@ def cleaning_add(message, user_name, day_of_week): :param str day_of_week: 追加する掃除曜日 """ if day_of_week not in DAY_OF_WEEK: - botsend(message, '曜日には `月` 、 `火` 、 `水` 、 `木` 、 `金` のいずれかを指定してください') + botsend(message, "曜日には `月` 、 `火` 、 `水` 、 `木` 、 `金` のいずれかを指定してください") return s = Session() slack_id = get_slack_id(s, user_name) if not slack_id: - botsend(message, '{}はSlackのユーザーとして存在しません'.format(user_name)) + botsend(message, "{}はSlackのユーザーとして存在しません".format(user_name)) return q = s.query(Cleaning).filter(Cleaning.slack_id == slack_id) if s.query(q.exists()).scalar(): - botsend(message, '{}は既に登録されています'.format(user_name)) + botsend(message, "{}は既に登録されています".format(user_name)) return s.add(Cleaning(slack_id=slack_id, day_of_week=DAY_OF_WEEK.index(day_of_week))) s.commit() - botsend(message, '{}を{}曜日の掃除当番に登録しました'.format(user_name, day_of_week)) + botsend(message, "{}を{}曜日の掃除当番に登録しました".format(user_name, day_of_week)) -@respond_to(r'^cleaning\s+del\s+(\S+)\s+(\S+)$') +@respond_to(r"^cleaning\s+del\s+(\S+)\s+(\S+)$") def cleaning_del(message, user_name, day_of_week): """指定した曜日の掃除当番からユーザーを削除する @@ -131,29 +133,31 @@ def cleaning_del(message, user_name, day_of_week): :param str day_of_week: 削除する掃除当番が登録されている曜日 """ if day_of_week not in DAY_OF_WEEK: - botsend(message, '曜日には `月` 、 `火` 、 `水` 、 `木` 、 `金` のいずれかを指定してください') + botsend(message, "曜日には `月` 、 `火` 、 `水` 、 `木` 、 `金` のいずれかを指定してください") return s = Session() slack_id = get_slack_id(s, user_name) if not slack_id: - botsend(message, '{}はSlackのユーザーとして存在しません'.format(user_name)) + botsend(message, "{}はSlackのユーザーとして存在しません".format(user_name)) return - cleaning_user = (s.query(Cleaning) - .filter(Cleaning.slack_id == slack_id) - .filter(Cleaning.day_of_week == DAY_OF_WEEK.index(day_of_week)) - .one_or_none()) + cleaning_user = ( + s.query(Cleaning) + .filter(Cleaning.slack_id == slack_id) + .filter(Cleaning.day_of_week == DAY_OF_WEEK.index(day_of_week)) + .one_or_none() + ) if cleaning_user: s.delete(cleaning_user) s.commit() - botsend(message, '{}を{}曜日の掃除当番から削除しました'.format(user_name, day_of_week)) + botsend(message, "{}を{}曜日の掃除当番から削除しました".format(user_name, day_of_week)) else: - botsend(message, '{}は{}曜日の掃除当番に登録されていません'.format(user_name, day_of_week)) + botsend(message, "{}は{}曜日の掃除当番に登録されていません".format(user_name, day_of_week)) -@respond_to(r'^cleaning\s+swap\s+(\S+)\s+(\S+)$') +@respond_to(r"^cleaning\s+swap\s+(\S+)\s+(\S+)$") def cleaning_swap(message, user_name1, user_name2): """登録された掃除当番のユーザーの掃除曜日を入れ替える @@ -167,36 +171,36 @@ def cleaning_swap(message, user_name1, user_name2): slack_id2 = get_slack_id(s, user_name2) if slack_id1 is None: - botsend(message, '{}はSlackのユーザーとして存在しません'.format(user_name1)) + botsend(message, "{}はSlackのユーザーとして存在しません".format(user_name1)) return if slack_id2 is None: - botsend(message, '{}はSlackのユーザーとして存在しません'.format(user_name2)) + botsend(message, "{}はSlackのユーザーとして存在しません".format(user_name2)) return if slack_id1 == slack_id2: - botsend(message, '{}と{}は同じSlackのユーザーです'.format(user_name1, user_name2)) + botsend(message, "{}と{}は同じSlackのユーザーです".format(user_name1, user_name2)) return - cleaning_user1 = (s.query(Cleaning) - .filter(Cleaning.slack_id == slack_id1) - .one_or_none()) - cleaning_user2 = (s.query(Cleaning) - .filter(Cleaning.slack_id == slack_id2) - .one_or_none()) + cleaning_user1 = ( + s.query(Cleaning).filter(Cleaning.slack_id == slack_id1).one_or_none() + ) + cleaning_user2 = ( + s.query(Cleaning).filter(Cleaning.slack_id == slack_id2).one_or_none() + ) if not cleaning_user1: - botsend(message, '{}は掃除当番に登録されていません'.format(user_name1)) + botsend(message, "{}は掃除当番に登録されていません".format(user_name1)) return if not cleaning_user2: - botsend(message, '{}は掃除当番に登録されていません'.format(user_name2)) + botsend(message, "{}は掃除当番に登録されていません".format(user_name2)) return cleaning_user1.slack_id = slack_id2 cleaning_user2.slack_id = slack_id1 s.commit() - botsend(message, '{}と{}の掃除当番を交換しました'.format(user_name1, user_name2)) + botsend(message, "{}と{}の掃除当番を交換しました".format(user_name1, user_name2)) -@respond_to(r'^cleaning\s+move\s+(\S+)\s+(\S+)$') +@respond_to(r"^cleaning\s+move\s+(\S+)\s+(\S+)$") def cleaning_move(message, user_name, day_of_week): """登録された掃除当番のユーザーの掃除曜日を移動させる @@ -205,23 +209,23 @@ def cleaning_move(message, user_name, day_of_week): :param str day_of_week: 移動先の曜日名 """ if day_of_week not in DAY_OF_WEEK: - botsend(message, '曜日には `月` 、 `火` 、 `水` 、 `木` 、 `金` のいずれかを指定してください') + botsend(message, "曜日には `月` 、 `火` 、 `水` 、 `木` 、 `金` のいずれかを指定してください") return s = Session() slack_id = get_slack_id(s, user_name) if slack_id is None: - botsend(message, '{}はSlackのユーザーとして存在しません'.format(user_name)) + botsend(message, "{}はSlackのユーザーとして存在しません".format(user_name)) return - cleaning_user = (s.query(Cleaning) - .filter(Cleaning.slack_id == slack_id) - .one_or_none()) + cleaning_user = ( + s.query(Cleaning).filter(Cleaning.slack_id == slack_id).one_or_none() + ) if not cleaning_user: - botsend(message, '{}は掃除当番に登録されていません'.format(user_name)) + botsend(message, "{}は掃除当番に登録されていません".format(user_name)) return cleaning_user.day_of_week = DAY_OF_WEEK.index(day_of_week) s.commit() - botsend(message, '{}の掃除当番の曜日を{}曜日に変更しました'.format(user_name, day_of_week)) + botsend(message, "{}の掃除当番の曜日を{}曜日に変更しました".format(user_name, day_of_week)) diff --git a/src/haro/plugins/cleaning_models.py b/src/haro/plugins/cleaning_models.py index 050eba3..805d501 100644 --- a/src/haro/plugins/cleaning_models.py +++ b/src/haro/plugins/cleaning_models.py @@ -5,9 +5,9 @@ class Cleaning(Base): - """掃除当番、各掃除の曜日割当の管理に使用されるコマンドのModel - """ - __tablename__ = 'cleaning' + """掃除当番、各掃除の曜日割当の管理に使用されるコマンドのModel""" + + __tablename__ = "cleaning" id = Column(Integer, primary_key=True) slack_id = Column(Unicode(11), nullable=False) diff --git a/src/haro/plugins/create.py b/src/haro/plugins/create.py index 15a6b65..a6d49fe 100644 --- a/src/haro/plugins/create.py +++ b/src/haro/plugins/create.py @@ -32,121 +32,110 @@ def command_patterns(message): :return list: 実装コマンドの一覧 """ commands = set() - for deco in ['respond_to', 'listen_to']: + for deco in ["respond_to", "listen_to"]: for re_compile in message._plugins.commands.get(deco): - commands.add(re_compile.pattern.split('\\s')[0].lstrip('^').rstrip('$')) + commands.add(re_compile.pattern.split("\\s")[0].lstrip("^").rstrip("$")) return commands class BaseCommandValidator(BaseArgValidator): - """createコマンドのバリデータの共通クラス - """ - skip_args = ['message', 'params'] + """createコマンドのバリデータの共通クラス""" + + skip_args = ["message", "params"] def has_command(self, command_name): - """コマンド名が登録済みか返す - """ + """コマンド名が登録済みか返す""" s = Session() - qs = (s.query(CreateCommand) - .filter(CreateCommand.name == command_name)) + qs = s.query(CreateCommand).filter(CreateCommand.name == command_name) return s.query(qs.exists()).scalar() def get_command(self, command_name): - """コマンド名が一致するCommandModelを返す - """ + """コマンド名が一致するCommandModelを返す""" s = Session() - command = (s.query(CreateCommand) - .filter(CreateCommand.name == command_name) - .one_or_none()) + command = ( + s.query(CreateCommand) + .filter(CreateCommand.name == command_name) + .one_or_none() + ) return command def handle_errors(self): for err_msg in self.errors.values(): - self.callargs['message'].send(err_msg) + self.callargs["message"].send(err_msg) class AddCommandValidator(BaseCommandValidator): - def clean_command_name(self, command_name): - """コマンド名に対してValidationを適用する - """ + """コマンド名に対してValidationを適用する""" if self.has_command(command_name): - raise ValidationError('`${}`コマンドは既に登録されています'.format(command_name)) + raise ValidationError("`${}`コマンドは既に登録されています".format(command_name)) - if command_name in command_patterns(self.callargs['message']): - raise ValidationError('`${}`は予約語です'.format(command_name)) + if command_name in command_patterns(self.callargs["message"]): + raise ValidationError("`${}`は予約語です".format(command_name)) return command_name class DelCommandValidator(BaseCommandValidator): - def clean_command_name(self, command_name): - """コマンド名に対してValidationを適用する - """ + """コマンド名に対してValidationを適用する""" if not self.has_command(command_name): - raise ValidationError('`${}`コマンドは登録されていません'.format(command_name)) + raise ValidationError("`${}`コマンドは登録されていません".format(command_name)) - if command_name in command_patterns(self.callargs['message']): - raise ValidationError('`${}`は予約語です'.format(command_name)) + if command_name in command_patterns(self.callargs["message"]): + raise ValidationError("`${}`は予約語です".format(command_name)) return command_name def clean_command(self): - """valid済のcommand名が存在すればCommandModelを返す - """ - command_name = self.cleaned_data.get('command_name') + """valid済のcommand名が存在すればCommandModelを返す""" + command_name = self.cleaned_data.get("command_name") return self.get_command(command_name) class RunCommandValidator(BaseCommandValidator): - def clean_command_name(self, command_name): - """コマンド名に対してValidationを適用する - """ - if command_name not in command_patterns(self.callargs['message']): + """コマンド名に対してValidationを適用する""" + if command_name not in command_patterns(self.callargs["message"]): if not self.has_command(command_name): - raise ValidationError('`${}`コマンドは登録されていません'.format(command_name)) + raise ValidationError("`${}`コマンドは登録されていません".format(command_name)) return command_name def clean_command(self): - """valid済のcommand名が存在すればCommandModelを返す - """ - command_name = self.cleaned_data.get('command_name') + """valid済のcommand名が存在すればCommandModelを返す""" + command_name = self.cleaned_data.get("command_name") return self.get_command(command_name) class ReturnTermCommandValidator(BaseCommandValidator): - EXCEPT_1WORD_COMMANDS = ['random', 'lunch', 'amesh', 'status'] + EXCEPT_1WORD_COMMANDS = ["random", "lunch", "amesh", "status"] def clean_command_name(self, command_name): - """コマンド名に対してValidationを適用する - """ + """コマンド名に対してValidationを適用する""" # 一文字コマンドをチェックから除外する # haroに登録されているコマンドを自動的に除外できるとよさそう if command_name in self.EXCEPT_1WORD_COMMANDS: return - if command_name not in command_patterns(self.callargs['message']): + if command_name not in command_patterns(self.callargs["message"]): if not self.has_command(command_name): - raise ValidationError('`${}`コマンドは登録されていません'.format(command_name)) + raise ValidationError("`${}`コマンドは登録されていません".format(command_name)) - if command_name in command_patterns(self.callargs['message']): - raise ValidationError('`${}`は予約語です'.format(command_name)) + if command_name in command_patterns(self.callargs["message"]): + raise ValidationError("`${}`は予約語です".format(command_name)) return command_name def clean_command(self): - """valid済のcommand名が存在すればCommandModelを返す - """ - command_name = self.cleaned_data.get('command_name') + """valid済のcommand名が存在すればCommandModelを返す""" + command_name = self.cleaned_data.get("command_name") return self.get_command(command_name) -@respond_to(r'^create\s+add\s+(\S+)$') +@respond_to(r"^create\s+add\s+(\S+)$") @register_arg_validator(AddCommandValidator) def add_command(message, command_name): """新たにコマンドを作成する @@ -156,13 +145,13 @@ def add_command(message, command_name): """ s = Session() - s.add(CreateCommand(name=command_name, creator=message.body['user'])) + s.add(CreateCommand(name=command_name, creator=message.body["user"])) s.commit() - botsend(message, '`${}`コマンドを登録しました'.format(command_name)) + botsend(message, "`${}`コマンドを登録しました".format(command_name)) -@respond_to(r'^create\s+del\s+(\S+)$') -@register_arg_validator(DelCommandValidator, ['command']) +@respond_to(r"^create\s+del\s+(\S+)$") +@register_arg_validator(DelCommandValidator, ["command"]) def del_command(message, command_name, command=None): """コマンドを削除する @@ -172,11 +161,11 @@ def del_command(message, command_name, command=None): s = Session() s.query(CreateCommand).filter(CreateCommand.id == command.id).delete() s.commit() - botsend(message, '`${}`コマンドを削除しました'.format(command_name)) + botsend(message, "`${}`コマンドを削除しました".format(command_name)) -@respond_to(r'^(\S+)$') -@register_arg_validator(ReturnTermCommandValidator, ['command']) +@respond_to(r"^(\S+)$") +@register_arg_validator(ReturnTermCommandValidator, ["command"]) def return_term(message, command_name, command=None): """コマンドに登録されている語録をランダムに返す @@ -190,11 +179,11 @@ def return_term(message, command_name, command=None): # #116 URLが展開されて欲しいのでpostMessageで返す webapisend(message, word) else: - botsend(message, '`${}`コマンドにはまだ語録が登録されていません'.format(command_name)) + botsend(message, "`${}`コマンドにはまだ語録が登録されていません".format(command_name)) -@respond_to(r'^(\S+)\s+(.+)') -@register_arg_validator(RunCommandValidator, ['command']) +@respond_to(r"^(\S+)\s+(.+)") +@register_arg_validator(RunCommandValidator, ["command"]) def run_command(message, command_name, params, command=None): """登録したコマンドに対して各種操作を行う @@ -211,19 +200,19 @@ def run_command(message, command_name, params, command=None): subcommand = data[0] try: - if subcommand == 'pop': + if subcommand == "pop": # 最後に登録された語録を削除 pop_term(message, command) - elif subcommand == 'list': + elif subcommand == "list": # 語録の一覧を返す get_term(message, command) - elif subcommand == 'search': + elif subcommand == "search": # 語録を検索 search_term(message, command, data[1]) - elif subcommand in ('del', 'delete', 'rm', 'remove'): + elif subcommand in ("del", "delete", "rm", "remove"): # 語録を削除 del_term(message, command, data[1]) - elif subcommand == 'add': + elif subcommand == "add": # 語録を追加 add_term(message, command, data[1]) else: @@ -241,20 +230,23 @@ def pop_term(message, command): :param str command: 登録済のコマンド名 """ s = Session() - term = (s.query(Term) - .filter(Term.create_command == command.id) - .filter(Term.creator == message.body['user']) - .order_by(Term.id.desc()) - .first()) + term = ( + s.query(Term) + .filter(Term.create_command == command.id) + .filter(Term.creator == message.body["user"]) + .order_by(Term.id.desc()) + .first() + ) name = command.name if term: s.delete(term) s.commit() - botsend(message, 'コマンド `${}` から「{}」を削除しました'.format(name, term.word)) + botsend(message, "コマンド `${}` から「{}」を削除しました".format(name, term.word)) else: - msg = ('コマンド `${0}` にあなたは語録を登録していません\n' - '`${0} add (語録)` で語録を登録してください'.format(name)) + msg = "コマンド `${0}` にあなたは語録を登録していません\n" "`${0} add (語録)` で語録を登録してください".format( + name + ) botsend(message, msg) @@ -266,14 +258,12 @@ def get_term(message, command): """ name = command.name if command.terms: - msg = ['コマンド `${}` の語録は {} 件あります'.format( - name, len(command.terms))] + msg = ["コマンド `${}` の語録は {} 件あります".format(name, len(command.terms))] for t in command.terms: msg.append(t.word) - botsend(message, '\n'.join(msg)) + botsend(message, "\n".join(msg)) else: - msg = ('コマンド `${0}` には語録が登録されていません\n' - '`${0} add (語録)` で語録を登録してください'.format(name)) + msg = "コマンド `${0}` には語録が登録されていません\n" "`${0} add (語録)` で語録を登録してください".format(name) botsend(message, msg) @@ -285,20 +275,20 @@ def search_term(message, command, keyword): :param str keyword: 登録済み語録から検索するキーワード """ s = Session() - terms = (s.query(Term) - .filter(Term.create_command == command.id) - .filter(Term.word.like('%' + keyword + '%'))) + terms = ( + s.query(Term) + .filter(Term.create_command == command.id) + .filter(Term.word.like("%" + keyword + "%")) + ) name = command.name if terms.count() > 0: - msg = ['コマンド `${}` の `{}` を含む語録は {} 件あります'.format( - name, keyword, terms.count())] + msg = ["コマンド `${}` の `{}` を含む語録は {} 件あります".format(name, keyword, terms.count())] for t in terms: msg.append(t.word) - botsend(message, '\n'.join(msg)) + botsend(message, "\n".join(msg)) else: - botsend(message, 'コマンド `${}` に `{}` を含む語録はありません'.format( - name, keyword)) + botsend(message, "コマンド `${}` に `{}` を含む語録はありません".format(name, keyword)) def del_term(message, command, word): @@ -309,18 +299,20 @@ def del_term(message, command, word): :param str word: 削除する語録 """ s = Session() - term = (s.query(Term) - .filter(Term.create_command == command.id) - .filter(Term.word == word) - .one_or_none()) + term = ( + s.query(Term) + .filter(Term.create_command == command.id) + .filter(Term.word == word) + .one_or_none() + ) name = command.name if term: s.delete(term) s.commit() - botsend(message, 'コマンド `${}` から「{}」を削除しました'.format(name, word)) + botsend(message, "コマンド `${}` から「{}」を削除しました".format(name, word)) else: - botsend(message, 'コマンド `${}` に「{}」は登録されていません'.format(name, word)) + botsend(message, "コマンド `${}` に「{}」は登録されていません".format(name, word)) def add_term(message, command, word): @@ -331,22 +323,24 @@ def add_term(message, command, word): :param str word: 登録する語録 """ s = Session() - term = (s.query(Term) - .select_from(join(Term, CreateCommand)) - .filter(CreateCommand.id == command.id) - .filter(Term.word == word) - .one_or_none()) + term = ( + s.query(Term) + .select_from(join(Term, CreateCommand)) + .filter(CreateCommand.id == command.id) + .filter(Term.word == word) + .one_or_none() + ) name = command.name if term: - botsend(message, 'コマンド `${}` に「{}」は登録済みです'.format(name, word)) + botsend(message, "コマンド `${}` に「{}」は登録済みです".format(name, word)) else: - s.add(Term(create_command=command.id, creator=message.body['user'], word=word)) + s.add(Term(create_command=command.id, creator=message.body["user"], word=word)) s.commit() - botsend(message, 'コマンド `${}` に「{}」を追加しました'.format(name, word)) + botsend(message, "コマンド `${}` に「{}」を追加しました".format(name, word)) -@respond_to(r'^create\s+help$') +@respond_to(r"^create\s+help$") def show_help_create_commands(message): """createコマンドのhelpを表示 diff --git a/src/haro/plugins/create_models.py b/src/haro/plugins/create_models.py index 19ba27c..ae44d94 100644 --- a/src/haro/plugins/create_models.py +++ b/src/haro/plugins/create_models.py @@ -6,27 +6,26 @@ class CreateCommand(Base): - """語録を登録するコマンドを管理するModel - """ - __tablename__ = 'create_command' + """語録を登録するコマンドを管理するModel""" + + __tablename__ = "create_command" id = Column(Integer, primary_key=True) name = Column(Unicode(100), nullable=False, unique=True) creator = Column(Unicode(100), nullable=False) ctime = Column(DateTime, default=datetime.datetime.now, nullable=False) - terms = relation('Term', backref='create_commands') + terms = relation("Term", backref="create_commands") class Term(Base): - """追加したコマンドに登録する語録を管理するModel - """ - __tablename__ = 'term' + """追加したコマンドに登録する語録を管理するModel""" + + __tablename__ = "term" id = Column(Integer, primary_key=True) - create_command = Column(Integer, ForeignKey( - 'create_command.id', - onupdate='CASCADE', - ondelete='CASCADE')) + create_command = Column( + Integer, ForeignKey("create_command.id", onupdate="CASCADE", ondelete="CASCADE") + ) word = Column(Unicode(1024), nullable=False) creator = Column(Unicode(100), nullable=False) ctime = Column(DateTime, default=datetime.datetime.now, nullable=False) diff --git a/src/haro/plugins/kintai.py b/src/haro/plugins/kintai.py index 90b7d90..934a161 100644 --- a/src/haro/plugins/kintai.py +++ b/src/haro/plugins/kintai.py @@ -22,45 +22,43 @@ - `$kintai help`: 勤怠コマンドの使い方を返す """ -DAY_OF_WEEK = '月火水木金土日' +DAY_OF_WEEK = "月火水木金土日" -@listen_to('おはよう|お早う|出社しました') +@listen_to("おはよう|お早う|出社しました") def replay_good_morning(message): - """「おはようごさいます」を返す - """ - botreply(message, 'おはようごさいます') + """「おはようごさいます」を返す""" + botreply(message, "おはようごさいます") -@listen_to('帰ります|かえります|退社します') +@listen_to("帰ります|かえります|退社します") def replay_you_did_good_today(message): - """「お疲れ様でした」を返す - """ - messages = ['お疲れ様でした'] * 99 - messages.append('うぇーい、おつかれちゃーん!') + """「お疲れ様でした」を返す""" + messages = ["お疲れ様でした"] * 99 + messages.append("うぇーい、おつかれちゃーん!") botreply(message, random.choice(messages)) -@respond_to(r'^kintai\s+start$') +@respond_to(r"^kintai\s+start$") def register_workon_time(message): """出社時刻を記録して挨拶を返すコマンド :param message: slackbotの各種パラメータを保持したclass """ - user_id = message.body['user'] + user_id = message.body["user"] register_worktime(user_id) - botreply(message, '出社時刻を記録しました') + botreply(message, "出社時刻を記録しました") -@respond_to(r'^kintai\s+end$') +@respond_to(r"^kintai\s+end$") def register_workoff_time(message): """退社時刻を記録して挨拶を返すコマンド :param message: slackbotの各種パラメータを保持したclass """ - user_id = message.body['user'] + user_id = message.body["user"] register_worktime(user_id, is_workon=False) - botreply(message, '退社時刻を記録しました') + botreply(message, "退社時刻を記録しました") def register_worktime(user_id, is_workon=True): @@ -72,11 +70,13 @@ def register_worktime(user_id, is_workon=True): today = datetime.date.today() s = Session() # SQLiteを使用している場合、castできないのでMySQLでdebugする事 - record = (s.query(KintaiHistory) - .filter(cast(KintaiHistory.registered_at, Date) == today) - .filter(KintaiHistory.user_id == user_id) - .filter(KintaiHistory.is_workon.is_(is_workon)) - .one_or_none()) + record = ( + s.query(KintaiHistory) + .filter(cast(KintaiHistory.registered_at, Date) == today) + .filter(KintaiHistory.user_id == user_id) + .filter(KintaiHistory.is_workon.is_(is_workon)) + .one_or_none() + ) if record: record.registered_at = datetime.datetime.now() else: @@ -84,79 +84,83 @@ def register_worktime(user_id, is_workon=True): s.commit() -@respond_to(r'^kintai\s+show$') +@respond_to(r"^kintai\s+show$") def show_kintai_history(message): """直近40日分の勤怠記録を表示します :param message: slackbotの各種パラメータを保持したclass """ - user_id = message.body['user'] + user_id = message.body["user"] today = datetime.date.today() target_day = today - datetime.timedelta(days=40) s = Session() - qs = (s.query(KintaiHistory) - .filter(KintaiHistory.user_id == user_id) - .filter(KintaiHistory.registered_at >= target_day) - .order_by(KintaiHistory.registered_at.asc())) + qs = ( + s.query(KintaiHistory) + .filter(KintaiHistory.user_id == user_id) + .filter(KintaiHistory.registered_at >= target_day) + .order_by(KintaiHistory.registered_at.asc()) + ) kintai = OrderedDict() for q in qs: day_of_week = DAY_OF_WEEK[q.registered_at.date().weekday()] - prefix_day = '{:%Y年%m月%d日}({})'.format(q.registered_at, day_of_week) - registered_at = '{:%H:%M:%S}'.format(q.registered_at) - kind = '出社' if q.is_workon else '退社' - kintai.setdefault(prefix_day, []).append('{} {}'.format(kind, - registered_at)) + prefix_day = "{:%Y年%m月%d日}({})".format(q.registered_at, day_of_week) + registered_at = "{:%H:%M:%S}".format(q.registered_at) + kind = "出社" if q.is_workon else "退社" + kintai.setdefault(prefix_day, []).append("{} {}".format(kind, registered_at)) rows = [] for prefix, registered_ats in kintai.items(): - sorted_times = ' '.join(sorted(registered_ats)) - rows.append('{} {}'.format(prefix, sorted_times)) + sorted_times = " ".join(sorted(registered_ats)) + rows.append("{} {}".format(prefix, sorted_times)) if not rows: - rows = ['勤怠記録はありません'] + rows = ["勤怠記録はありません"] user_name = get_user_name(user_id) - botsend(message, '{}の勤怠:\n{}'.format(user_name, '\n'.join(rows))) + botsend(message, "{}の勤怠:\n{}".format(user_name, "\n".join(rows))) -@respond_to(r'^kintai\s+csv$') -@respond_to(r'^kintai\s+csv\s+(\d{4}/\d{1,2})$') +@respond_to(r"^kintai\s+csv$") +@respond_to(r"^kintai\s+csv\s+(\d{4}/\d{1,2})$") def show_kintai_history_csv(message, time=None): """指定した月の勤怠記録をCSV形式で返す :param message: slackbotの各種パラメータを保持したclass :param str time: `/` 区切りの年月(例: 2016/1) """ - user_id = message.body['user'] + user_id = message.body["user"] if time: - year_str, month_str = time.split('/') + year_str, month_str = time.split("/") else: now = datetime.datetime.now() - year_str, month_str = now.strftime('%Y'), now.strftime('%m') + year_str, month_str = now.strftime("%Y"), now.strftime("%m") year, month = int(year_str), int(month_str) if not 1 <= month <= 12: - botsend(message, '指定した対象月は存在しません') + botsend(message, "指定した対象月は存在しません") return s = Session() - qs = (s.query(KintaiHistory) - .filter(KintaiHistory.user_id == user_id) - .filter(func.extract('year', KintaiHistory.registered_at) == year) - .filter(func.extract('month', KintaiHistory.registered_at) == month)) + qs = ( + s.query(KintaiHistory) + .filter(KintaiHistory.user_id == user_id) + .filter(func.extract("year", KintaiHistory.registered_at) == year) + .filter(func.extract("month", KintaiHistory.registered_at) == month) + ) kintai = defaultdict(list) for q in qs: - registered_at = q.registered_at.strftime('%Y-%m-%d') - kintai[registered_at].append((q.is_workon, - '{:%H:%M:%S}'.format(q.registered_at))) + registered_at = q.registered_at.strftime("%Y-%m-%d") + kintai[registered_at].append( + (q.is_workon, "{:%H:%M:%S}".format(q.registered_at)) + ) rows = [] for day in range(1, monthrange(year, month)[1] + 1): - aligin_date = '{}-{:02d}-{:02d}'.format(year, month, day) - workon, workoff = '', '' + aligin_date = "{}-{:02d}-{:02d}".format(year, month, day) + workon, workoff = "", "" for d in sorted(kintai[aligin_date]): if d[0]: workon = d[1] @@ -169,16 +173,16 @@ def show_kintai_history_csv(message, time=None): w.writerows(rows) param = { - 'token': settings.API_TOKEN, - 'channels': message.body['channel'], - 'title': '勤怠記録' + "token": settings.API_TOKEN, + "channels": message.body["channel"], + "title": "勤怠記録", } - requests.post(settings.FILE_UPLOAD_URL, - params=param, - files={'file': output.getvalue()}) + requests.post( + settings.FILE_UPLOAD_URL, params=param, files={"file": output.getvalue()} + ) -@respond_to(r'^kintai\s+help$') +@respond_to(r"^kintai\s+help$") def show_help_kintai_commands(message): """勤怠コマンドのhelpを表示 diff --git a/src/haro/plugins/kintai_models.py b/src/haro/plugins/kintai_models.py index d41de3e..3bb759a 100644 --- a/src/haro/plugins/kintai_models.py +++ b/src/haro/plugins/kintai_models.py @@ -10,11 +10,10 @@ class KintaiHistory(Base): :param Base: `sqlalchemy.ext.declarative.api.DeclarativeMeta` を 継承したclass """ - __tablename__ = 'kintai_history' + + __tablename__ = "kintai_history" id = Column(Integer, primary_key=True) user_id = Column(Unicode(100), nullable=False) is_workon = Column(Boolean, default=True) - registered_at = Column(DateTime, - default=datetime.datetime.now, - nullable=False) + registered_at = Column(DateTime, default=datetime.datetime.now, nullable=False) diff --git a/src/haro/plugins/kudo.py b/src/haro/plugins/kudo.py index 5b58a37..cf8076c 100644 --- a/src/haro/plugins/kudo.py +++ b/src/haro/plugins/kudo.py @@ -11,9 +11,9 @@ """ -@listen_to(r'^(.*)\s*(?` というstr型で渡ってくるので対応 - if get_user_name(name.lstrip('<@').rstrip('>')): - name = get_user_name(name.lstrip('<@').rstrip('>')) + if get_user_name(name.lstrip("<@").rstrip(">")): + name = get_user_name(name.lstrip("<@").rstrip(">")) s = Session() - kudo = (s.query(KudoHistory) - .filter(KudoHistory.name == name) - .filter(KudoHistory.from_user_id == slack_id) - .one_or_none()) + kudo = ( + s.query(KudoHistory) + .filter(KudoHistory.name == name) + .filter(KudoHistory.from_user_id == slack_id) + .one_or_none() + ) if kudo is None: # name ×from_user_id の組み合わせが存在していない -> 新規登録 @@ -48,17 +50,17 @@ def update_kudo(message, names): kudo.delta = kudo.delta + 1 s.commit() - q = (s.query( - func.sum(KudoHistory.delta).label('total_count')) - .filter(KudoHistory.name == name)) + q = s.query(func.sum(KudoHistory.delta).label("total_count")).filter( + KudoHistory.name == name + ) total_count = q.one().total_count name_list.append((name, total_count)) - msg = ['({}: 通算 {})'.format(n, tc) for n, tc in name_list] - botsend(message, '\n'.join(msg)) + msg = ["({}: 通算 {})".format(n, tc) for n, tc in name_list] + botsend(message, "\n".join(msg)) -@respond_to(r'^kudo\s+help$') +@respond_to(r"^kudo\s+help$") def show_help_alias_commands(message): """Kudoコマンドのhelpを表示 diff --git a/src/haro/plugins/kudo_models.py b/src/haro/plugins/kudo_models.py index 436f1de..780eae6 100644 --- a/src/haro/plugins/kudo_models.py +++ b/src/haro/plugins/kudo_models.py @@ -5,13 +5,13 @@ class KudoHistory(Base): - """kudoコマンドの管理に使用されるModel - """ - __tablename__ = 'kudo_history' + """kudoコマンドの管理に使用されるModel""" + + __tablename__ = "kudo_history" id = Column(Integer, primary_key=True) name = Column(Unicode(100), nullable=False) from_user_id = Column(Unicode(100), nullable=False) delta = Column(Integer, default=0, nullable=False) ctime = Column(DateTime, default=datetime.datetime.now, nullable=False) - UniqueConstraint('name', 'from_user_id', name='name_from_user_id') + UniqueConstraint("name", "from_user_id", name="name_from_user_id") diff --git a/src/haro/plugins/lunch.py b/src/haro/plugins/lunch.py index 12a3c30..f77c124 100644 --- a/src/haro/plugins/lunch.py +++ b/src/haro/plugins/lunch.py @@ -8,16 +8,16 @@ from slackbot.bot import respond_to from haro.botmessage import botsend -KML_SOURCE = 'https://www.google.com/maps/d/u/0/kml?hl=en&mid=1J4U-QXOe1Zi_4Lw5UxaL8AriG6M&lid=zubJL41y6fLI.k8KrINTXzJI4&forcekml=1' # NOQA -MAPS_URL_BASE = 'http://maps.google.com/maps/ms?ie=UTF&msa=0&msid=101243938118389113627.00047d5491d28f02d4f57&z=19&iwloc=A&ll=' # NOQA +KML_SOURCE = "https://www.google.com/maps/d/u/0/kml?hl=en&mid=1J4U-QXOe1Zi_4Lw5UxaL8AriG6M&lid=zubJL41y6fLI.k8KrINTXzJI4&forcekml=1" # NOQA +MAPS_URL_BASE = "http://maps.google.com/maps/ms?ie=UTF&msa=0&msid=101243938118389113627.00047d5491d28f02d4f57&z=19&iwloc=A&ll=" # NOQA BP_COORDINATES = (35.68641, 139.70343) -HELP = ''' +HELP = """ - `$lunch`: オフィス近辺のお店情報返す - `$lunch `: 指定したキーワードのお店情報を返す - `$lunch `: 指定したキーワードと検索距離のお店情報を返す - `$lunch help`: このコマンドの使い方を返す -''' +""" def parse_kml_to_json(url): @@ -32,7 +32,7 @@ def parse_kml_to_json(url): kml_str = md.parseString(r.content) layers = k2g.build_layers(kml_str) - places = layers[0]['features'] + places = layers[0]["features"] return places @@ -45,8 +45,10 @@ def get_distance_from_office(destination): :return: 距離(メートル単位) """ office_coordinates = BP_COORDINATES - destination_coordinates = (destination['geometry']['coordinates'][1], - destination['geometry']['coordinates'][0]) + destination_coordinates = ( + destination["geometry"]["coordinates"][1], + destination["geometry"]["coordinates"][0], + ) distance = vincenty(office_coordinates, destination_coordinates).meters @@ -68,14 +70,14 @@ def lunch(keyword, distance=500): # NOQA: ignore=C901 try: places = parse_kml_to_json(KML_SOURCE) except requests.exceptions.HTTPError as e: - return '''ランチの検索をしたが、次の問題が発生してしまいました。ごめんなさい:cry:\n```{}```'''.format(e) + return """ランチの検索をしたが、次の問題が発生してしまいました。ごめんなさい:cry:\n```{}```""".format(e) # 検索キーワード指定があれば、該当する店舗だけの候補列を作る if keyword: filtered_places = [] for p in places: try: - if keyword in p['properties']['description']: + if keyword in p["properties"]["description"]: filtered_places.append(p) except KeyError: pass @@ -84,8 +86,10 @@ def lunch(keyword, distance=500): # NOQA: ignore=C901 places = filtered_places[:] else: - return '''[{}]の店舗情報はありませんでした。\n - '''.format(keyword) + return """[{}]の店舗情報はありませんでした。\n + """.format( + keyword + ) # placesに候補がある限り繰り返す while places: @@ -95,22 +99,24 @@ def lunch(keyword, distance=500): # NOQA: ignore=C901 elif get_distance_from_office(place) > walking_distance: places.remove(place) else: - msg = '''\n今日のランチはココ!\n>>>*{}*\n{}\n`{}{},{}`\n_オフィスから{}m_ - '''.format(place['properties']['name'], - place['properties']['description'], - MAPS_URL_BASE, - place['geometry']['coordinates'][1], - place['geometry']['coordinates'][0], - get_distance_from_office(place)) - msg = re.sub(r'<[^<]+?>', '\n', msg) + msg = """\n今日のランチはココ!\n>>>*{}*\n{}\n`{}{},{}`\n_オフィスから{}m_ + """.format( + place["properties"]["name"], + place["properties"]["description"], + MAPS_URL_BASE, + place["geometry"]["coordinates"][1], + place["geometry"]["coordinates"][0], + get_distance_from_office(place), + ) + msg = re.sub(r"<[^<]+?>", "\n", msg) return msg return "{}m以内の店舗は見つかりませんでした。".format(walking_distance) -@respond_to('^lunch$') -@respond_to(r'^lunch\s+(\S+)$') -@respond_to(r'^lunch\s+(\S+)\s+(\d+)$') +@respond_to("^lunch$") +@respond_to(r"^lunch\s+(\S+)$") +@respond_to(r"^lunch\s+(\S+)\s+(\d+)$") def show_lunch(message, keyword=None, distance=500): """Lunchコマンドの結果を表示する @@ -118,7 +124,7 @@ def show_lunch(message, keyword=None, distance=500): :param keyword: 検索キーワード :param distance: 検索範囲 (default 500m) """ - if keyword == 'help': + if keyword == "help": return distance = int(distance) @@ -126,7 +132,7 @@ def show_lunch(message, keyword=None, distance=500): botsend(message, lunch(keyword, distance)) -@respond_to(r'^lunch\s+help$') +@respond_to(r"^lunch\s+help$") def show_help_lunch_commands(message): """lunchコマンドのhelpを表示 diff --git a/src/haro/plugins/misc.py b/src/haro/plugins/misc.py index 6fc62ee..a3bf56f 100644 --- a/src/haro/plugins/misc.py +++ b/src/haro/plugins/misc.py @@ -4,24 +4,22 @@ from haro.botmessage import botsend -@respond_to(r'^shuffle\s+(.*)') +@respond_to(r"^shuffle\s+(.*)") def shuffle(message, words): - """指定したキーワードをシャッフルして返す - """ + """指定したキーワードをシャッフルして返す""" words = words.split() if len(words) == 1: - botsend(message, 'キーワードを複数指定してください\n`$shuffle word1 word2...`') + botsend(message, "キーワードを複数指定してください\n`$shuffle word1 word2...`") else: random.shuffle(words) - botsend(message, ' '.join(words)) + botsend(message, " ".join(words)) -@respond_to(r'^choice\s+(.*)') +@respond_to(r"^choice\s+(.*)") def choice(message, words): - """指定したキーワードから一つを選んで返す - """ + """指定したキーワードから一つを選んで返す""" words = words.split() if len(words) == 1: - botsend(message, 'キーワードを複数指定してください\n`$choice word1 word2...`') + botsend(message, "キーワードを複数指定してください\n`$choice word1 word2...`") else: botsend(message, random.choice(words)) diff --git a/src/haro/plugins/random.py b/src/haro/plugins/random.py index 2f13a7a..d45164a 100644 --- a/src/haro/plugins/random.py +++ b/src/haro/plugins/random.py @@ -7,17 +7,17 @@ from haro.botmessage import botsend -HELP = ''' +HELP = """ - `$random`: チャンネルにいるメンバーからランダムに一人を選ぶ - `$random active`: チャンネルにいるactiveなメンバーからランダムに一人を選ぶ - `$random help`: randomコマンドの使い方を返す -''' +""" logger = logging.getLogger(__name__) -@respond_to('^random$') -@respond_to(r'^random\s+(active|help)$') +@respond_to("^random$") +@respond_to(r"^random\s+(active|help)$") def random_command(message, subcommand=None): """ チャンネルにいるメンバーからランダムに一人を選んで返す @@ -27,37 +27,39 @@ def random_command(message, subcommand=None): - https://api.slack.com/methods/users.info """ - if subcommand == 'help': + if subcommand == "help": botsend(message, HELP) return # チャンネルのメンバー一覧を取得 - channel = message.body['channel'] + channel = message.body["channel"] webapi = slacker.Slacker(settings.API_TOKEN) try: cinfo = webapi.conversations.members(channel) - members = cinfo.body['members'] + members = cinfo.body["members"] except slacker.Error: - logger.exception('An error occurred while fetching members: channel=%s', channel) + logger.exception( + "An error occurred while fetching members: channel=%s", channel + ) return # bot の id は除く - bot_id = message._client.login_data['self']['id'] + bot_id = message._client.login_data["self"]["id"] members.remove(bot_id) member_id = None - if subcommand != 'active': + if subcommand != "active": member_id = random.choice(members) else: # active が指定されている場合は presence を確認する random.shuffle(members) for member in members: presence = webapi.users.get_presence(member_id) - if presence.body['presence'] == 'active': + if presence.body["presence"] == "active": member_id = member break user_info = webapi.users.info(member_id) - name = user_info.body['user']['name'] - botsend(message, '{} さん、君に決めた!'.format(name)) + name = user_info.body["user"]["name"] + botsend(message, "{} さん、君に決めた!".format(name)) diff --git a/src/haro/plugins/redbull.py b/src/haro/plugins/redbull.py index ac0e6d3..008836c 100644 --- a/src/haro/plugins/redbull.py +++ b/src/haro/plugins/redbull.py @@ -13,7 +13,7 @@ from haro.plugins.redbull_models import RedbullHistory from haro.slack import get_user_name -_cache = {'token': None} +_cache = {"token": None} HELP = """ - `$redbull count`: RedBullの残り本数を表示する @@ -25,21 +25,21 @@ """ -@respond_to(r'^redbull\s+count$') +@respond_to(r"^redbull\s+count$") def count_redbull_stock(message): """現在のRedBullの在庫本数を返すコマンド :param message: slackbotの各種パラメータを保持したclass """ s = Session() - q = s.query(func.sum(RedbullHistory.delta).label('stock_number')) + q = s.query(func.sum(RedbullHistory.delta).label("stock_number")) stock_number = q.one().stock_number if stock_number is None: stock_number = 0 - botsend(message, 'レッドブル残り {} 本'.format(stock_number)) + botsend(message, "レッドブル残り {} 本".format(stock_number)) -@respond_to(r'^redbull\s+(-?\d+)$') +@respond_to(r"^redbull\s+(-?\d+)$") def manage_redbull_stock(message, delta): """RedBullの本数の増減を行うコマンド @@ -49,7 +49,7 @@ def manage_redbull_stock(message, delta): DBは投入の場合正数、消費の場合は負数を記録する """ delta = -int(delta) - user_id = message.body['user'] + user_id = message.body["user"] user_name = get_user_name(user_id) s = Session() @@ -57,45 +57,48 @@ def manage_redbull_stock(message, delta): s.commit() if delta > 0: - botsend(message, 'レッドブルが{}により{}本投入されました'.format(user_name, delta)) + botsend(message, "レッドブルが{}により{}本投入されました".format(user_name, delta)) else: - botsend(message, 'レッドブルが{}により{}本消費されました'.format(user_name, -delta)) + botsend(message, "レッドブルが{}により{}本消費されました".format(user_name, -delta)) -@respond_to(r'^redbull\s+history$') +@respond_to(r"^redbull\s+history$") def show_user_redbull_history(message): """RedBullのUserごとの消費履歴を返すコマンド :param message: slackbotの各種パラメータを保持したclass """ - user_id = message.body['user'] + user_id = message.body["user"] user_name = get_user_name(user_id) s = Session() - qs = (s.query(RedbullHistory) - .filter(RedbullHistory.user_id == user_id, - RedbullHistory.delta < 0) - .order_by(RedbullHistory.id.asc())) + qs = ( + s.query(RedbullHistory) + .filter(RedbullHistory.user_id == user_id, RedbullHistory.delta < 0) + .order_by(RedbullHistory.id.asc()) + ) tmp = [] for line in qs: - tmp.append('[{:%Y年%m月%d日}] {}本'.format(line.ctime, -line.delta)) + tmp.append("[{:%Y年%m月%d日}] {}本".format(line.ctime, -line.delta)) - ret = '消費履歴はありません' + ret = "消費履歴はありません" if tmp: - ret = '\n'.join(tmp) + ret = "\n".join(tmp) - botsend(message, '{}の消費したレッドブル:\n{}'.format(user_name, ret)) + botsend(message, "{}の消費したレッドブル:\n{}".format(user_name, ret)) -@respond_to(r'^redbull\s+csv$') +@respond_to(r"^redbull\s+csv$") def show_redbull_history_csv(message): """RedBullの月単位の消費履歴をCSVに出力する :param message: slackbotの各種パラメータを保持したclass """ s = Session() - consume_hisotry = (s.query(RedbullHistory) - .filter(RedbullHistory.delta < 0) - .order_by(RedbullHistory.id.desc())) + consume_hisotry = ( + s.query(RedbullHistory) + .filter(RedbullHistory.delta < 0) + .order_by(RedbullHistory.id.desc()) + ) # func.month関数を使って月ごとでgroupby count書けるが、 # SQLiteにはMONTH()関数がないので月集計はPythonで処理する @@ -105,24 +108,24 @@ def grouper(item): ret = [] for ((year, month), items) in groupby(consume_hisotry, grouper): count = -sum(item.delta for item in items) - ret.append(['{}/{}'.format(year, month), str(count)]) + ret.append(["{}/{}".format(year, month), str(count)]) output = StringIO() w = csv.writer(output) w.writerows(ret) param = { - 'token': settings.API_TOKEN, - 'channels': message.body['channel'], - 'title': 'RedBull History Check' + "token": settings.API_TOKEN, + "channels": message.body["channel"], + "title": "RedBull History Check", } - requests.post(settings.FILE_UPLOAD_URL, - params=param, - files={'file': output.getvalue()}) + requests.post( + settings.FILE_UPLOAD_URL, params=param, files={"file": output.getvalue()} + ) -@respond_to(r'^redbull\s+clear$') -@respond_to(r'^redbull\s+clear\s+(\w+)$') +@respond_to(r"^redbull\s+clear$") +@respond_to(r"^redbull\s+clear\s+(\w+)$") def clear_redbull_history(message, token=None): """RedBullの履歴データを削除するコマンド @@ -134,23 +137,25 @@ def clear_redbull_history(message, token=None): :param str token: `$redbull clear` の後に入力されたトークン """ if token is None: - _cache['token'] = ''.join(choice(ascii_letters) for i in range(16)) - botsend(message, '履歴をDBからすべてクリアします。' - '続けるには\n`$redbull clear {}`\nと書いてください' - .format(_cache['token'])) + _cache["token"] = "".join(choice(ascii_letters) for i in range(16)) + botsend( + message, + "履歴をDBからすべてクリアします。" + "続けるには\n`$redbull clear {}`\nと書いてください".format(_cache["token"]), + ) return - if token == _cache['token']: - _cache['token'] = None + if token == _cache["token"]: + _cache["token"] = None s = Session() s.query(RedbullHistory).delete() s.commit() - botsend(message, '履歴をクリアしました') + botsend(message, "履歴をクリアしました") else: - botsend(message, 'コマンドが一致しないため履歴をクリアできませんでした') + botsend(message, "コマンドが一致しないため履歴をクリアできませんでした") -@respond_to(r'^redbull\s+help$') +@respond_to(r"^redbull\s+help$") def show_help_redbull_commands(message): """RedBullコマンドのhelpを表示 diff --git a/src/haro/plugins/redbull_models.py b/src/haro/plugins/redbull_models.py index 5b395a1..4515141 100644 --- a/src/haro/plugins/redbull_models.py +++ b/src/haro/plugins/redbull_models.py @@ -10,7 +10,8 @@ class RedbullHistory(Base): :param Base: `sqlalchemy.ext.declarative.api.DeclarativeMeta` を 継承したclass """ - __tablename__ = 'redbull_history' + + __tablename__ = "redbull_history" id = Column(Integer, primary_key=True) user_id = Column(Unicode(100), nullable=False) diff --git a/src/haro/plugins/redmine.py b/src/haro/plugins/redmine.py index fef955c..e9be381 100644 --- a/src/haro/plugins/redmine.py +++ b/src/haro/plugins/redmine.py @@ -6,17 +6,17 @@ from haro.botmessage import botsend from haro.plugins.redmine_models import RedmineUser, ProjectChannel -RESPONSE_ERROR = 'Redmineにアクセスできませんでした。' -NO_CHANNEL_PERMISSIONS = '{}は{}で表示できません。' -NO_TEXT = '(本文なし)' - -API_KEY_SET = 'APIキーを保存しました。' -INVALID_API_KEY = 'APIキーは無効です。' -CHANNEL_REGISTERED = 'Redmineの{}プロジェクトをチャンネルに追加しました。' -CHANNEL_UNREGISTERED = 'Redmineの{}プロジェクトをチャンネルから削除しました。' -CHANNEL_ALREADY_REGISTERED = 'このSlackチャンネルは既に登録されています。' -CHANNEL_NOT_REGISTERED = 'このSlackチャンネルは{}プロジェクトに登録されていません。' -PROJECT_NOT_FOUND = 'プロジェクトは見つかりませんでした。' +RESPONSE_ERROR = "Redmineにアクセスできませんでした。" +NO_CHANNEL_PERMISSIONS = "{}は{}で表示できません。" +NO_TEXT = "(本文なし)" + +API_KEY_SET = "APIキーを保存しました。" +INVALID_API_KEY = "APIキーは無効です。" +CHANNEL_REGISTERED = "Redmineの{}プロジェクトをチャンネルに追加しました。" +CHANNEL_UNREGISTERED = "Redmineの{}プロジェクトをチャンネルから削除しました。" +CHANNEL_ALREADY_REGISTERED = "このSlackチャンネルは既に登録されています。" +CHANNEL_NOT_REGISTERED = "このSlackチャンネルは{}プロジェクトに登録されていません。" +PROJECT_NOT_FOUND = "プロジェクトは見つかりませんでした。" HELP = """ - `/msg @haro $redmine key `: 自分のRedmineのAPIキーを登録する @@ -49,29 +49,35 @@ def project_channel_from_identifier(api_key, identifier, session): except (ForbiddenError, ResourceNotFoundError): return None, None - chan = session.query(ProjectChannel).filter(ProjectChannel.project_id == project.id). \ - one_or_none() + chan = ( + session.query(ProjectChannel) + .filter(ProjectChannel.project_id == project.id) + .one_or_none() + ) return project, chan def user_from_message(message, session): # message.bodyにuserが含まれている場合のみ反応する - if not message.body.get('user'): + if not message.body.get("user"): return - user_id = message.body['user'] + user_id = message.body["user"] - user = session.query(RedmineUser).filter(RedmineUser.user_id == user_id).one_or_none() + user = ( + session.query(RedmineUser).filter(RedmineUser.user_id == user_id).one_or_none() + ) return user -@respond_to(r'^redmine\s+help$') +@respond_to(r"^redmine\s+help$") def show_help_redmine_commands(message): - """Redmineコマンドのhelpを表示 - """ + """Redmineコマンドのhelpを表示""" botsend(message, HELP) -@listen_to(r'issues\/(\d{2,}\#note\-\d+)|issues\/(\d{2,})|[^a-zA-Z/`\n`][t](\d{2,})|^t(\d{2,})') +@listen_to( + r"issues\/(\d{2,}\#note\-\d+)|issues\/(\d{2,})|[^a-zA-Z/`\n`][t](\d{2,})|^t(\d{2,})" +) def show_ticket_information(message, *ticket_ids): # NOQA: R701, C901 """Redmineのチケット情報を参照する. @@ -81,11 +87,13 @@ def show_ticket_information(message, *ticket_ids): # NOQA: R701, C901 s = Session() channel = message.channel - channel_id = channel._body['id'] + channel_id = channel._body["id"] user = user_from_message(message, s) if not user: return - channels = s.query(ProjectChannel.id).filter(ProjectChannel.channels.contains(channel_id)) + channels = s.query(ProjectChannel.id).filter( + ProjectChannel.channels.contains(channel_id) + ) if not s.query(channels.exists()).scalar(): return @@ -96,8 +104,8 @@ def show_ticket_information(message, *ticket_ids): # NOQA: R701, C901 noteno = None note_suffix = "" - if '#note-' in ticket_id: - ticket_id, noteno = ticket_id.split('#note-') + if "#note-" in ticket_id: + ticket_id, noteno = ticket_id.split("#note-") note_suffix = "#note-{}".format(noteno) try: @@ -107,11 +115,16 @@ def show_ticket_information(message, *ticket_ids): # NOQA: R701, C901 return proj_id = ticket.project.id - proj_room = s.query(ProjectChannel).filter(ProjectChannel.project_id == proj_id) \ + proj_room = ( + s.query(ProjectChannel) + .filter(ProjectChannel.project_id == proj_id) .one_or_none() + ) - if not proj_room or channel_id not in proj_room.channels.split(','): - botsend(message, NO_CHANNEL_PERMISSIONS.format(ticket_id, channel._body['name'])) + if not proj_room or channel_id not in proj_room.channels.split(","): + botsend( + message, NO_CHANNEL_PERMISSIONS.format(ticket_id, channel._body["name"]) + ) return if noteno: @@ -119,12 +132,12 @@ def show_ticket_information(message, *ticket_ids): # NOQA: R701, C901 # Redmine 側で変更がなければ問題ないけど、 # values には #note-n に相当するidがはいっていないので # id でソートして順番を保証している - notes = sorted(ticket.journals.values(), key=lambda d: d['id']) + notes = sorted(ticket.journals.values(), key=lambda d: d["id"]) for i, v in enumerate(notes, start=1): if str(i) == noteno: # コメントの本文があれば取得する - if v.get('notes'): - description = v['notes'] + if v.get("notes"): + description = v["notes"] # コメント本文がなかったら書き換えられるよう仮文言としている if not description: description = NO_TEXT @@ -132,26 +145,32 @@ def show_ticket_information(message, *ticket_ids): # NOQA: R701, C901 # デフォルトでは説明欄の本文を使用する description = ticket.description or NO_TEXT - text = "#{ticketno}{noteno}: [{assigned_to}][{priority}][{status}] {title}".format( - ticketno=ticket_id, - noteno=note_suffix, - assigned_to=getattr(ticket, "assigned_to", "担当者なし"), - priority=getattr(ticket, "priority", "-"), - status=getattr(ticket, "status", "-"), - title=ticket.subject, + text = ( + "#{ticketno}{noteno}: [{assigned_to}][{priority}][{status}] {title}".format( + ticketno=ticket_id, + noteno=note_suffix, + assigned_to=getattr(ticket, "assigned_to", "担当者なし"), + priority=getattr(ticket, "priority", "-"), + status=getattr(ticket, "status", "-"), + title=ticket.subject, + ) ) url = "{}{}".format(ticket.url, note_suffix) sc = message._client.webapi - res = sc.chat.post_message(channel_id, "<{}|{}>".format(url, text), as_user=True) - sc.chat.post_message(channel_id, description, as_user=True, thread_ts=res.body['ts']) + res = sc.chat.post_message( + channel_id, "<{}|{}>".format(url, text), as_user=True + ) + sc.chat.post_message( + channel_id, description, as_user=True, thread_ts=res.body["ts"] + ) -@respond_to(r'^redmine\s+key\s+(\S+)$') +@respond_to(r"^redmine\s+key\s+(\S+)$") def register_key(message, api_key): s = Session() - if not message.body.get('user'): + if not message.body.get("user"): return # APIキーは最大40文字となっている。 @@ -160,7 +179,7 @@ def register_key(message, api_key): botsend(message, INVALID_API_KEY) return - user_id = message.body['user'] + user_id = message.body["user"] user = s.query(RedmineUser).filter(RedmineUser.user_id == user_id).one_or_none() if not user: @@ -171,7 +190,7 @@ def register_key(message, api_key): botsend(message, API_KEY_SET) -@respond_to(r'^redmine\s+add\s+([a-zA-Z0-9_-]+)$') +@respond_to(r"^redmine\s+add\s+([a-zA-Z0-9_-]+)$") def register_room(message, project_identifier): """RedmineのプロジェクトとSlackチャンネルを繋ぐ. @@ -180,12 +199,14 @@ def register_room(message, project_identifier): """ s = Session() channel = message.channel - channel_id = channel._body['id'] + channel_id = channel._body["id"] user = user_from_message(message, s) if not user: return - project, project_channel = project_channel_from_identifier(user.api_key, project_identifier, s) + project, project_channel = project_channel_from_identifier( + user.api_key, project_identifier, s + ) if not project: botsend(message, PROJECT_NOT_FOUND) return @@ -205,18 +226,20 @@ def register_room(message, project_identifier): botsend(message, CHANNEL_ALREADY_REGISTERED) -@respond_to(r'^redmine\s+remove\s+([a-zA-Z0-9_-]+)$') +@respond_to(r"^redmine\s+remove\s+([a-zA-Z0-9_-]+)$") def unregister_room(message, project_identifier): s = Session() channel = message.channel - channel_id = channel._body['id'] + channel_id = channel._body["id"] user = user_from_message(message, s) if not user: return - project, project_channel = project_channel_from_identifier(user.api_key, project_identifier, s) + project, project_channel = project_channel_from_identifier( + user.api_key, project_identifier, s + ) if not project: botsend(message, PROJECT_NOT_FOUND) return @@ -225,7 +248,9 @@ def unregister_room(message, project_identifier): return try: - channels = project_channel.channels.split(",") if project_channel.channels else [] + channels = ( + project_channel.channels.split(",") if project_channel.channels else [] + ) channels.remove(channel_id) project_channel.channels = ",".join(channels) s.commit() diff --git a/src/haro/plugins/redmine_models.py b/src/haro/plugins/redmine_models.py index ebe8335..361a4e4 100644 --- a/src/haro/plugins/redmine_models.py +++ b/src/haro/plugins/redmine_models.py @@ -8,16 +8,26 @@ class RedmineUser(Base): __tablename__ = "redmine_users" id = Column(Integer, primary_key=True) - user_id = Column(Unicode(9), nullable=False, unique=True, doc=""" + user_id = Column( + Unicode(9), + nullable=False, + unique=True, + doc=""" Slackのユーザid 例: U023BECGF - """) - api_key = Column(Unicode(40), nullable=False, unique=True, doc=""" + """, + ) + api_key = Column( + Unicode(40), + nullable=False, + unique=True, + doc=""" RedmineのAPIのKey https://project.beproud.jp/redmine/my/accountの右側で見つかります。 例: d1d567978001e4f884524a8941a9bbe6a8be87ac - """) + """, + ) class ProjectChannel(Base): @@ -26,10 +36,17 @@ class ProjectChannel(Base): __tablename__ = "redmine_projectchannel" id = Column(Integer, primary_key=True) - project_id = Column(Integer, unique=True, doc=""" + project_id = Column( + Integer, + unique=True, + doc=""" Redmineのprojectのid - """) - channels = Column(Unicode(249), nullable=False, doc=""" + """, + ) + channels = Column( + Unicode(249), + nullable=False, + doc=""" CSV channel id list. i.e. Projectのチケットは定義したチャネルしかに表示しない 例: "C0AGP8QQH,C0AGP8QQZ" @@ -37,4 +54,5 @@ class ProjectChannel(Base): 25チャネルまで登録できる. 9 (チャネルの長さ) * 25 = 225 + 24 (,の数) = 249 - """) + """, + ) diff --git a/src/haro/plugins/resource.py b/src/haro/plugins/resource.py index 86ce474..c4e06b1 100644 --- a/src/haro/plugins/resource.py +++ b/src/haro/plugins/resource.py @@ -17,11 +17,14 @@ COMMANDS = ( "help", "add", - "del", "delete", "rm", "remove", + "del", + "delete", + "rm", + "remove", ) -@respond_to(r'^status\s+help$') +@respond_to(r"^status\s+help$") def show_help(message): """Statusコマンドのhelpを表示 @@ -39,9 +42,14 @@ def show_resources(message): s = Session() - resources = s.query(Resource).filter( - Resource.channel_id == channel_id, - ).order_by(Resource.id.asc()).all() + resources = ( + s.query(Resource) + .filter( + Resource.channel_id == channel_id, + ) + .order_by(Resource.id.asc()) + .all() + ) statuses = [] for resource in resources: if resource.status is None: @@ -56,10 +64,10 @@ def show_resources(message): botsend(message, "\n".join(statuses)) -respond_to('^status$')(show_resources) +respond_to("^status$")(show_resources) -@respond_to(r'^status\s+add\s+(\S+)$') +@respond_to(r"^status\s+add\s+(\S+)$") def add_resource(message, name): """リソースの追加 @@ -68,22 +76,28 @@ def add_resource(message, name): channel_id = message.body["channel"] s = Session() - resource = s.query(Resource).filter( - Resource.channel_id == channel_id, - Resource.name == name, - ).one_or_none() + resource = ( + s.query(Resource) + .filter( + Resource.channel_id == channel_id, + Resource.name == name, + ) + .one_or_none() + ) if not resource: - s.add(Resource( - channel_id=channel_id, - name=name, - status=None, - )) + s.add( + Resource( + channel_id=channel_id, + name=name, + status=None, + ) + ) s.commit() show_resources(message) -@respond_to(r'^status\s+(del|delete|rm|remove)\s+(\S+)$') +@respond_to(r"^status\s+(del|delete|rm|remove)\s+(\S+)$") def remove_resource(message, _, name): """リソースの削除 @@ -93,10 +107,14 @@ def remove_resource(message, _, name): channel_id = message.body["channel"] s = Session() - resource = s.query(Resource).filter( - Resource.channel_id == channel_id, - Resource.name == name, - ).one_or_none() + resource = ( + s.query(Resource) + .filter( + Resource.channel_id == channel_id, + Resource.name == name, + ) + .one_or_none() + ) if resource: s.delete(resource) s.commit() @@ -104,7 +122,7 @@ def remove_resource(message, _, name): show_resources(message) -@respond_to(r'^status\s+(\S+)$') +@respond_to(r"^status\s+(\S+)$") def unset_resource_status(message, name): """リソースの設定を初期値に戻す @@ -117,10 +135,14 @@ def unset_resource_status(message, name): channel_id = message.body["channel"] s = Session() - resource = s.query(Resource).filter( - Resource.channel_id == channel_id, - Resource.name == name, - ).one_or_none() + resource = ( + s.query(Resource) + .filter( + Resource.channel_id == channel_id, + Resource.name == name, + ) + .one_or_none() + ) if resource: resource.status = None s.commit() @@ -128,7 +150,7 @@ def unset_resource_status(message, name): show_resources(message) -@respond_to(r'^status\s+(\S+)\s+(\S+)$') +@respond_to(r"^status\s+(\S+)\s+(\S+)$") def set_resource_status(message, name, value): """リソースの設定 @@ -142,10 +164,14 @@ def set_resource_status(message, name, value): channel_id = message.body["channel"] s = Session() - resource = s.query(Resource).filter( - Resource.channel_id == channel_id, - Resource.name == name, - ).one_or_none() + resource = ( + s.query(Resource) + .filter( + Resource.channel_id == channel_id, + Resource.name == name, + ) + .one_or_none() + ) if resource: resource.status = value s.commit() diff --git a/src/haro/plugins/resource_models.py b/src/haro/plugins/resource_models.py index 7e6faac..d5e39a4 100644 --- a/src/haro/plugins/resource_models.py +++ b/src/haro/plugins/resource_models.py @@ -5,9 +5,9 @@ class Resource(Base): - """StatusコマンドのリソースのModel - """ - __tablename__ = 'resource' + """StatusコマンドのリソースのModel""" + + __tablename__ = "resource" id = Column(Integer, primary_key=True) channel_id = Column(Unicode(249), nullable=False) diff --git a/src/haro/plugins/thx.py b/src/haro/plugins/thx.py index 35a26dd..140f5bd 100644 --- a/src/haro/plugins/thx.py +++ b/src/haro/plugins/thx.py @@ -35,23 +35,24 @@ def find_thx(s, text): hint_names = [] not_matched = [] - thx_matcher = re.compile(r'(?P.+)[ \t\f\v]*(?.+)', - re.MULTILINE) + thx_matcher = re.compile( + r"(?P.+)[ \t\f\v]*(?.+)", re.MULTILINE + ) for thx in thx_matcher.finditer(text): - user_names = [x for x in thx.group('user_names').split(' ') if x] + user_names = [x for x in thx.group("user_names").split(" ") if x] for name in user_names: - if get_user_name(name.lstrip('<@').rstrip('>')): - slack_id = name.lstrip('<@').rstrip('>') + if get_user_name(name.lstrip("<@").rstrip(">")): + slack_id = name.lstrip("<@").rstrip(">") else: slack_id = get_slack_id(s, name) if slack_id: - word_map_names_dict.setdefault( - thx.group('word'), [] - ).append((slack_id, name)) + word_map_names_dict.setdefault(thx.group("word"), []).append( + (slack_id, name) + ) else: # 一番近いユーザー名を算出 - names = [profile['name'] for profile in get_users_info().values()] + names = [profile["name"] for profile in get_users_info().values()] hint = get_close_matches(name, names) if hint: hint_names.append(hint[0]) @@ -62,7 +63,7 @@ def find_thx(s, text): return word_map_names_dict, hint_names, not_matched -@listen_to(r'.*\s*(?= period[(month - 1) % 12])) % 12 - d = r.json()['horoscope'][today][n] - for s in ['total', 'love', 'money', 'job']: + d = r.json()["horoscope"][today][n] + for s in ["total", "love", "money", "job"]: d[s] = star(d[s]) return """\ {rank}位 {sign} @@ -26,10 +26,12 @@ def uranai(birthday): 仕事運: {job} ラッキーカラー: {color} ラッキーアイテム: {item} -{content}""".format(**d) +{content}""".format( + **d + ) -@respond_to(r'^uranai\s+(\d{4})$') +@respond_to(r"^uranai\s+(\d{4})$") def show_uranai_commands(message, birthday): """Uranaiコマンドの結果を表示 diff --git a/src/haro/plugins/version.py b/src/haro/plugins/version.py index 5a49a53..b483582 100644 --- a/src/haro/plugins/version.py +++ b/src/haro/plugins/version.py @@ -4,17 +4,17 @@ from slackbot.bot import respond_to from haro.botmessage import botsend -VERSION_PAT = re.compile(r'Release Notes - [\d-]+') -LOG_PAT = re.compile(r'-\s[#[\w\W]+]\s[\w\W]+') +VERSION_PAT = re.compile(r"Release Notes - [\d-]+") +LOG_PAT = re.compile(r"-\s[#[\w\W]+]\s[\w\W]+") HELP = """ `$version`: デプロイされているChangeLog.rstから最新の更新内容を表示する """ def read_change_log(): - change_log_path = '{}/ChangeLog.rst'.format(settings.PROJECT_ROOT) + change_log_path = "{}/ChangeLog.rst".format(settings.PROJECT_ROOT) try: - with open(change_log_path, 'r', encoding='utf-8') as f: + with open(change_log_path, "r", encoding="utf-8") as f: change_log = f.read() return change_log except FileNotFoundError: @@ -25,34 +25,32 @@ def version(): try: body = read_change_log() except FileNotFoundError: - message = 'リリースノートが見つかりません' + message = "リリースノートが見つかりません" return message - release_notes = body.strip().split('\n') + release_notes = body.strip().split("\n") latest_row = 0 for idx, line in enumerate(release_notes): if VERSION_PAT.match(line): latest_row = idx break version = release_notes[latest_row].strip() - log = '' - for line in release_notes[latest_row + 2:]: + log = "" + for line in release_notes[latest_row + 2 :]: if LOG_PAT.match(line): - log += '{}\n'.format(line) + log += "{}\n".format(line) else: break - message = '{}\n'.format(version) + \ - '--------------------------\n' + \ - '{}'.format(log) + message = "{}\n".format(version) + "--------------------------\n" + "{}".format(log) return message -@respond_to(r'^version$') +@respond_to(r"^version$") def show_version_commands(message): botsend(message, version()) -@respond_to(r'^version\s+help$') +@respond_to(r"^version\s+help$") def show_help_version_commands(message): """versionコマンドのhelpを表示 diff --git a/src/haro/plugins/water.py b/src/haro/plugins/water.py index b2ba3ab..6d6ac8d 100644 --- a/src/haro/plugins/water.py +++ b/src/haro/plugins/water.py @@ -7,40 +7,38 @@ from haro.plugins.water_models import WaterHistory from slackbot_settings import WATER_EMPTY_TO, WATER_ORDER_NUM -HELP = ''' +HELP = """ - `$water count`: 現在の残数を返す - `$water num`: 水を取り替えた時に使用。指定した数だけ残数を減らす(numが負数の場合、増やす) - `$water history `: 指定した件数分の履歴を返す(default=10) - `$water help`: このコマンドの使い方を返す -''' +""" -@respond_to(r'^water\s+count$') +@respond_to(r"^water\s+count$") def count_water_stock(message): """現在の水の在庫本数を返すコマンド :param message: slackbotの各種パラメータを保持したclass """ s = Session() - stock_number, latest_ctime = ( - s.query(func.sum(WaterHistory.delta), - func.max(case(whens=(( - WaterHistory.delta != 0, - WaterHistory.ctime),), else_=None))).first() - ) + stock_number, latest_ctime = s.query( + func.sum(WaterHistory.delta), + func.max( + case(whens=((WaterHistory.delta != 0, WaterHistory.ctime),), else_=None) + ), + ).first() if stock_number: # SQLiteの場合文字列で渡ってくるので対応 if not isinstance(latest_ctime, datetime.datetime): - latest_ctime = datetime.datetime.strptime(latest_ctime, - '%Y-%m-%d %H:%M:%S') - botsend(message, '残数: {}本 ({:%Y年%m月%d日} 追加)' - .format(stock_number, latest_ctime)) + latest_ctime = datetime.datetime.strptime(latest_ctime, "%Y-%m-%d %H:%M:%S") + botsend(message, "残数: {}本 ({:%Y年%m月%d日} 追加)".format(stock_number, latest_ctime)) else: - botsend(message, '管理履歴はありません') + botsend(message, "管理履歴はありません") -@respond_to(r'^water\s+(-?\d+)$') +@respond_to(r"^water\s+(-?\d+)$") def manage_water_stock(message, delta): """水の本数の増減を行うコマンド @@ -51,59 +49,56 @@ def manage_water_stock(message, delta): """ delta = -int(delta) if not delta: - botsend(message, '0は指定できません') + botsend(message, "0は指定できません") return - user_id = message.body['user'] + user_id = message.body["user"] s = Session() s.add(WaterHistory(user_id=user_id, delta=delta)) s.commit() - q = s.query(func.sum(WaterHistory.delta).label('stock_number')) + q = s.query(func.sum(WaterHistory.delta).label("stock_number")) stock_number = q.one().stock_number if delta < 0: - text = 'ウォーターサーバーのボトルを{}本取りかえました。' + text = "ウォーターサーバーのボトルを{}本取りかえました。" if stock_number <= int(WATER_ORDER_NUM) and WATER_EMPTY_TO: - text += '\n 残数: {}です。注文お願いします。' + text += "\n 残数: {}です。注文お願いします。" else: - text += '(残数: {}本)' + text += "(残数: {}本)" botsend(message, text.format(-delta, stock_number)) else: - botsend(message, 'ウォーターサーバーのボトルを{}本追加しました。(残数: {}本)' - .format(delta, stock_number)) + botsend( + message, "ウォーターサーバーのボトルを{}本追加しました。(残数: {}本)".format(delta, stock_number) + ) -@respond_to(r'^water\s+history$') -@respond_to(r'^water\s+history\s+(\d+)$') -def show_water_history(message, limit='10'): +@respond_to(r"^water\s+history$") +@respond_to(r"^water\s+history\s+(\d+)$") +def show_water_history(message, limit="10"): """水の管理履歴を返すコマンド :param message: slackbotの各種パラメータを保持したclass """ s = Session() - qs = (s.query(WaterHistory) - .order_by(WaterHistory.id.desc()) - .limit(limit)) + qs = s.query(WaterHistory).order_by(WaterHistory.id.desc()).limit(limit) tmp = [] for line in qs: if line.delta > 0: - tmp.append('[{:%Y年%m月%d日}] {}本 追加' - .format(line.ctime, line.delta)) + tmp.append("[{:%Y年%m月%d日}] {}本 追加".format(line.ctime, line.delta)) else: - tmp.append('[{:%Y年%m月%d日}] {}本 取替' - .format(line.ctime, -line.delta)) + tmp.append("[{:%Y年%m月%d日}] {}本 取替".format(line.ctime, -line.delta)) - ret = '管理履歴はありません' + ret = "管理履歴はありません" if tmp: - ret = '\n'.join(tmp) + ret = "\n".join(tmp) - botsend(message, '水の管理履歴:\n{}'.format(ret)) + botsend(message, "水の管理履歴:\n{}".format(ret)) -@respond_to(r'^water\s+help$') +@respond_to(r"^water\s+help$") def show_help_water_commands(message): """waterコマンドのhelpを表示 diff --git a/src/haro/plugins/water_models.py b/src/haro/plugins/water_models.py index 03a05f1..a127daf 100644 --- a/src/haro/plugins/water_models.py +++ b/src/haro/plugins/water_models.py @@ -10,7 +10,8 @@ class WaterHistory(Base): :param Base: `sqlalchemy.ext.declarative.api.DeclarativeMeta` を 継承したclass """ - __tablename__ = 'water_history' + + __tablename__ = "water_history" id = Column(Integer, primary_key=True) user_id = Column(Unicode(100), nullable=False) diff --git a/src/haro/slack.py b/src/haro/slack.py index 8f33b59..2c97584 100644 --- a/src/haro/slack.py +++ b/src/haro/slack.py @@ -20,14 +20,14 @@ def get_users_info(): users = {} webapi = slacker.Slacker(settings.API_TOKEN) try: - for d in webapi.users.list().body['members']: + for d in webapi.users.list().body["members"]: profile = { - 'name': d['name'], - 'display_name': d['profile']['display_name'], + "name": d["name"], + "display_name": d["profile"]["display_name"], } - users[d['id']] = profile + users[d["id"]] = profile except slacker.Error: - logger.error('Cannot connect to Slack') + logger.error("Cannot connect to Slack") return users @@ -41,7 +41,7 @@ def get_user_name(user_id): profile = users.get(user_id) if not profile: return - return profile['name'] + return profile["name"] def get_user_display_name(user_id): @@ -52,10 +52,10 @@ def get_user_display_name(user_id): """ users = get_users_info() profile = users.get(user_id) - if profile['display_name']: - return profile['display_name'] + if profile["display_name"]: + return profile["display_name"] # display_nameはoptional、空ならユーザー名を返す - return profile['name'] + return profile["name"] def get_slack_id_by_name(name): @@ -66,5 +66,5 @@ def get_slack_id_by_name(name): """ users = get_users_info() for slack_id, profile in users.items(): - if profile['name'] == name: + if profile["name"] == name: return slack_id diff --git a/src/redmine_notification.py b/src/redmine_notification.py index 35b820b..6aebf49 100644 --- a/src/redmine_notification.py +++ b/src/redmine_notification.py @@ -40,13 +40,11 @@ logger.setLevel(logging.INFO) # file headerを作る -handler = logging.FileHandler('../logs/redmine_notification.log') +handler = logging.FileHandler("../logs/redmine_notification.log") handler.setLevel(logging.INFO) # logging formatを作る -formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) # handlerをloggerに加える @@ -58,11 +56,10 @@ def get_ticket_information(): - """Redmineのチケット情報とチケットと結びついているSlackチャンネルを取得 - """ + """Redmineのチケット情報とチケットと結びついているSlackチャンネルを取得""" redmine = Redmine(REDMINE_URL, key=REDMINE_API_KEY) # すべてのチケットを取得 - issues = redmine.issue.filter(status_id='open', sort='due_date') + issues = redmine.issue.filter(status_id="open", sort="due_date") projects_past_due_date = defaultdict(list) projects_close_to_due_date = defaultdict(list) @@ -73,14 +70,14 @@ def get_ticket_information(): all_proj_channels = s.query(ProjectChannel).all() for issue in issues: # due_date属性とdue_dateがnoneの場合は除外 - if not getattr(issue, 'due_date', None): + if not getattr(issue, "due_date", None): continue proj_id = issue.project.id # 全てのプロジェクトチャンネルを獲得 channels = get_proj_channels(proj_id, all_proj_channels) if not channels: # slack channelが設定されていないissueは無視する continue - elif not getattr(issue, 'assigned_to', None): + elif not getattr(issue, "assigned_to", None): # 担当者が設定されていないチケットを各プロジェクトごとに抽出 projects_assigned_to_is_none[issue.project.id].append(issue) elif issue.due_date < today: @@ -91,14 +88,9 @@ def get_ticket_information(): projects_close_to_due_date[proj_id].append(issue) # 各プロジェクトのチケット通知をSlackチャンネルに送る。 - send_ticket_info_to_channels(projects_past_due_date, - all_proj_channels, - True) - send_ticket_info_to_channels(projects_close_to_due_date, - all_proj_channels, - False) - send_assigned_to_is_not_set_tickets(projects_assigned_to_is_none, - all_proj_channels) + send_ticket_info_to_channels(projects_past_due_date, all_proj_channels, True) + send_ticket_info_to_channels(projects_close_to_due_date, all_proj_channels, False) + send_assigned_to_is_not_set_tickets(projects_assigned_to_is_none, all_proj_channels) def display_issue(issues, has_assigned_to=True): @@ -112,26 +104,27 @@ def display_issue(issues, has_assigned_to=True): text = [] for issue in issues: if has_assigned_to: - text.append('- {}: <{}|#{}> {} (<@{}>)'.format(issue.due_date, - issue.url, - issue.id, - issue.subject, - issue.assigned_to)) + text.append( + "- {}: <{}|#{}> {} (<@{}>)".format( + issue.due_date, + issue.url, + issue.id, + issue.subject, + issue.assigned_to, + ) + ) else: - text.append('- {}: <{}|#{}> {}'.format(issue.due_date, - issue.url, - issue.id, - issue.subject)) - - attachment = [{ - "color": "#F44336", - "text": "\n".join(text) - }] + text.append( + "- {}: <{}|#{}> {}".format( + issue.due_date, issue.url, issue.id, issue.subject + ) + ) + + attachment = [{"color": "#F44336", "text": "\n".join(text)}] return attachment -def send_ticket_info_to_channels(projects, all_proj_channels, - is_past_due_date): +def send_ticket_info_to_channels(projects, all_proj_channels, is_past_due_date): """チャンネルを取得し、チケット情報を各Slackチャンネルごとに通知する。 :param projects: 期限が切れたプロジェクト、期限が切れそうなプロジェクトのdict @@ -145,9 +138,9 @@ def send_ticket_info_to_channels(projects, all_proj_channels, # プロジェクトごとのチケット数カウントを取得 issue_count = len(projects[project]) if is_past_due_date: # 期限切れチケット - message = '期限が切れたチケットは{}件です\n'.format(issue_count) + message = "期限が切れたチケットは{}件です\n".format(issue_count) else: # 期限切れそうなチケット - message = 'もうすぐ期限が切れそうなチケットは{}件です\n'.format(issue_count) + message = "もうすぐ期限が切れそうなチケットは{}件です\n".format(issue_count) # 通知メッセージをformat attachments = display_issue(projects[project]) for channel in channels: @@ -173,7 +166,7 @@ def get_proj_channels(project_id, all_proj_channels): """ for proj_room in all_proj_channels: if project_id == proj_room.project_id: - return proj_room.channels.split(',') if proj_room.channels else [] + return proj_room.channels.split(",") if proj_room.channels else [] def send_slack_message(channel, attachments, message): @@ -184,15 +177,15 @@ def send_slack_message(channel, attachments, message): """ sc = WebClient(API_TOKEN) return sc.api_call( - 'chat.postMessage', + "chat.postMessage", json={ - 'channel': channel, - 'text': message, - 'as_user': "false", - 'icon_emoji': EMOJI, - 'username': BOTNAME, - 'attachments': attachments - } + "channel": channel, + "text": message, + "as_user": "false", + "icon_emoji": EMOJI, + "username": BOTNAME, + "attachments": attachments, + }, ) @@ -219,39 +212,37 @@ def send_slack_message_per_sec(channel, attachments, message): # メッセージが通知されたかをチェックする # メッセージ通知が成功なら、response["ok"]がTrue if response["ok"]: - JST = timezone(timedelta(hours=+9), 'JST') - time_stamp = datetime.fromtimestamp( - float(response["message"]["ts"]), - JST - ) - logger.info( - "Message posted successfully: {}".format(time_stamp) - ) + JST = timezone(timedelta(hours=+9), "JST") + time_stamp = datetime.fromtimestamp(float(response["message"]["ts"]), JST) + logger.info("Message posted successfully: {}".format(time_stamp)) def get_argparser(): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, - description=dedent('''\ + description=dedent( + """\ 説明: - haroの設定ファイルを読み込んだ後にredmine_notification.pyを実行''')) + haroの設定ファイルを読み込んだ後にredmine_notification.pyを実行""" + ), + ) - parser.add_argument('-c', '--config', - type=argparse.FileType('r'), - default='alembic/conf.ini', - help='ini形式のファイルをファイルパスで指定します') + parser.add_argument( + "-c", + "--config", + type=argparse.FileType("r"), + default="alembic/conf.ini", + help="ini形式のファイルをファイルパスで指定します", + ) return parser def notify_error(text): message = "期限切れチケット通知処理でエラーが発生しました" - attachment = [{ - "color": "#F44336", - "text": text - }] + attachment = [{"color": "#F44336", "text": text}] # bp-bot-dev チャンネルに通知 - send_slack_message('C0106MV2C4F', attachment, message) + send_slack_message("C0106MV2C4F", attachment, message) def main(): @@ -267,14 +258,14 @@ def main(): conf = ConfigParser() conf.read_file(args.config) # 環境変数で指定したいため ini ファイルでなくここで追記 - conf["alembic"]['sqlalchemy.url'] = SQLALCHEMY_URL - conf["alembic"]['sqlalchemy.echo'] = SQLALCHEMY_ECHO + conf["alembic"]["sqlalchemy.url"] = SQLALCHEMY_URL + conf["alembic"]["sqlalchemy.echo"] = SQLALCHEMY_ECHO if SQLALCHEMY_POOL_SIZE: - conf["alembic"]['sqlalchemy.pool_size'] = SQLALCHEMY_POOL_SIZE - if not conf.has_section('alembic'): - raise NoSectionError('alembic') + conf["alembic"]["sqlalchemy.pool_size"] = SQLALCHEMY_POOL_SIZE + if not conf.has_section("alembic"): + raise NoSectionError("alembic") - init_dbsession(conf['alembic']) + init_dbsession(conf["alembic"]) get_ticket_information() diff --git a/src/run.py b/src/run.py index fa49814..279db89 100644 --- a/src/run.py +++ b/src/run.py @@ -17,14 +17,20 @@ def get_argparser(): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, - description=dedent('''\ + description=dedent( + """\ 説明: - haroの設定ファイルを読み込んだ後にslackbotを起動します''')) - - parser.add_argument('-c', '--config', - type=argparse.FileType('r'), - default='alembic/conf.ini', - help='ini形式のファイルをファイルパスで指定します') + haroの設定ファイルを読み込んだ後にslackbotを起動します""" + ), + ) + + parser.add_argument( + "-c", + "--config", + type=argparse.FileType("r"), + default="alembic/conf.ini", + help="ini形式のファイルをファイルパスで指定します", + ) return parser @@ -35,7 +41,7 @@ def haro_default_replay(message): :param message: slackbot.dispatcher.Message """ - botsend(message, 'コマンドが不明です') + botsend(message, "コマンドが不明です") def main(): @@ -53,14 +59,14 @@ def main(): conf = ConfigParser() conf.read_file(args.config) # 環境変数で指定したいため ini ファイルでなくここで追記 - conf["alembic"]['sqlalchemy.url'] = SQLALCHEMY_URL - conf["alembic"]['sqlalchemy.echo'] = SQLALCHEMY_ECHO + conf["alembic"]["sqlalchemy.url"] = SQLALCHEMY_URL + conf["alembic"]["sqlalchemy.echo"] = SQLALCHEMY_ECHO if SQLALCHEMY_POOL_SIZE: - conf["alembic"]['sqlalchemy.pool_size'] = SQLALCHEMY_POOL_SIZE - if not conf.has_section('alembic'): - raise NoSectionError('alembic') + conf["alembic"]["sqlalchemy.pool_size"] = SQLALCHEMY_POOL_SIZE + if not conf.has_section("alembic"): + raise NoSectionError("alembic") - init_dbsession(conf['alembic']) + init_dbsession(conf["alembic"]) bot = Bot() bot.run() diff --git a/src/slackbot_settings.py b/src/slackbot_settings.py index 24eb65e..2413063 100644 --- a/src/slackbot_settings.py +++ b/src/slackbot_settings.py @@ -3,7 +3,7 @@ """ import os -TRUE_VALUES = ('true', 'yes', 1) +TRUE_VALUES = ("true", "yes", 1) def is_true(arg): @@ -15,41 +15,42 @@ def is_true(arg): ##### SLACK ##### # SlackのAPIトークン # https://my.slack.com/services/new/bot で生成 -API_TOKEN = os.environ['SLACK_API_TOKEN'] +API_TOKEN = os.environ["SLACK_API_TOKEN"] # 読み込むpluginのリスト PLUGINS = [ - 'haro.plugins', + "haro.plugins", ] # コマンドの接頭語 -ALIASES = '$' +ALIASES = "$" # コマンド失敗時のエラー通知 -if os.getenv('SLACK_ERRORS_TO'): - ERRORS_TO = os.environ['SLACK_ERRORS_TO'] +if os.getenv("SLACK_ERRORS_TO"): + ERRORS_TO = os.environ["SLACK_ERRORS_TO"] # Slack ファイルアップロードAPI -FILE_UPLOAD_URL = 'https://slack.com/api/files.upload' +FILE_UPLOAD_URL = "https://slack.com/api/files.upload" # Redmine チケットAPI -REDMINE_URL = os.environ['REDMINE_URL'] -REDMINE_API_KEY = os.environ['REDMINE_API_KEY'] +REDMINE_URL = os.environ["REDMINE_URL"] +REDMINE_API_KEY = os.environ["REDMINE_API_KEY"] ##### HARO ##### # デバッグモードにするとログが出るので、開発時には便利 -DEBUG = is_true(os.environ['HARO_DEBUG']) +DEBUG = is_true(os.environ["HARO_DEBUG"]) if DEBUG: import logging + logging.basicConfig(level=logging.DEBUG) # haroのプロジェクトルート -PROJECT_ROOT = os.environ['PROJECT_ROOT'] +PROJECT_ROOT = os.environ["PROJECT_ROOT"] ##### DB ##### -SQLALCHEMY_URL = os.environ['SQLALCHEMY_URL'] -SQLALCHEMY_ECHO = os.environ['SQLALCHEMY_ECHO'] -SQLALCHEMY_POOL_SIZE = os.environ.get('SQLALCHEMY_POOL_SIZE') +SQLALCHEMY_URL = os.environ["SQLALCHEMY_URL"] +SQLALCHEMY_ECHO = os.environ["SQLALCHEMY_ECHO"] +SQLALCHEMY_POOL_SIZE = os.environ.get("SQLALCHEMY_POOL_SIZE") # Waterコマンドメンション先 -WATER_EMPTY_TO = os.environ.get('WATER_EMPTY_TO') -WATER_ORDER_NUM = os.environ.get('WATER_ORDER_NUM', 2) +WATER_EMPTY_TO = os.environ.get("WATER_EMPTY_TO") +WATER_ORDER_NUM = os.environ.get("WATER_ORDER_NUM", 2) diff --git a/tests/__init__.py b/tests/__init__.py index b8834d5..e33a493 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,4 @@ # XXX: haro ディレクトリを参照するため。他に方法ある? import sys -sys.path.append('src') + +sys.path.append("src") diff --git a/tests/conftest.py b/tests/conftest.py index 8ba73e3..90fa01f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,10 +3,11 @@ from tests.db import DatabaseManager -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def test_engine(): """Session-wide test database""" from sqlalchemy import create_engine + engine = create_engine("sqlite:///test.sqlite") return engine diff --git a/tests/test_arg_validator.py b/tests/test_arg_validator.py index db36686..e932823 100644 --- a/tests/test_arg_validator.py +++ b/tests/test_arg_validator.py @@ -3,7 +3,7 @@ import pytest -class TestBaseArgValidator(): +class TestBaseArgValidator: """BaseArgValidator のテスト 基底クラスなので基本的になにも起こらない @@ -11,6 +11,7 @@ class TestBaseArgValidator(): def _makeOne(self, *args, **kwargs): from haro.arg_validator import BaseArgValidator + return BaseArgValidator(*args, **kwargs) def test_handle_errors(self): @@ -23,13 +24,13 @@ def test_is_valid(self): def test_is_valid_given_extra(self): """予期しない extras が与えられた場合にエラーになる""" - validator = self._makeOne({}, ['extra_arg']) + validator = self._makeOne({}, ["extra_arg"]) with pytest.raises(AttributeError): assert not validator.is_valid() -class TestArgValidator(): +class TestArgValidator: """BaseArgvalidator を継承したクラスのテスト""" def _makeOne(self, *args, **kwargs): @@ -45,39 +46,43 @@ def clean_odd(self, value): return value def clean_pow(self): - return self.cleaned_data['odd'] * self.cleaned_data['odd'] + return self.cleaned_data["odd"] * self.cleaned_data["odd"] return ArgValidator(*args, **kwargs) def test_clean_called(self): """対応するcleanメソッドが呼ばれる""" - callargs = {'odd': 1} + callargs = {"odd": 1} validator = self._makeOne(callargs) validator.clean_odd = MagicMock() validator.is_valid() - validator.clean_odd.assert_called_with(callargs['odd']) - - @pytest.mark.parametrize('callargs, expected', [ - ({'odd': 1}, True), - ({'odd': 2}, False), - ]) + validator.clean_odd.assert_called_with(callargs["odd"]) + + @pytest.mark.parametrize( + "callargs, expected", + [ + ({"odd": 1}, True), + ({"odd": 2}, False), + ], + ) def test_is_valid(self, callargs, expected): validator = self._makeOne(callargs) assert validator.is_valid() == expected if not validator.is_valid(): - assert 'odd' in validator.errors - assert validator.errors['odd'] == "value should be odd" + assert "odd" in validator.errors + assert validator.errors["odd"] == "value should be odd" def test_is_valid_with_extra(self): - validator = self._makeOne({'odd': 3}, extras=['pow']) + validator = self._makeOne({"odd": 3}, extras=["pow"]) assert validator.is_valid() - assert 'pow' in validator.cleaned_data - assert validator.cleaned_data['pow'] == 9 + assert "pow" in validator.cleaned_data + assert validator.cleaned_data["pow"] == 9 -class TestRegisterArgValidator(): +class TestRegisterArgValidator: def _callFUT(self, *args, **kwargs): from haro.arg_validator import register_arg_validator + return register_arg_validator(*args, **kwargs) def _validator_class(self): @@ -97,6 +102,7 @@ def clean_odd(self, value): def test_ok(self): def _f(odd): return odd + f = self._callFUT(self._validator_class())(_f) assert f(1) == 1 assert f(2) is None diff --git a/tests/test_plugins_kudo.py b/tests/test_plugins_kudo.py index ab83cc2..572cabf 100644 --- a/tests/test_plugins_kudo.py +++ b/tests/test_plugins_kudo.py @@ -6,48 +6,51 @@ class TestUpdateKudo: - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def target_pattern(self): - with mock.patch('slackbot.bot.listen_to') as mock_listen_to: + with mock.patch("slackbot.bot.listen_to") as mock_listen_to: from haro.plugins import kudo + importlib.reload(kudo) # ensure applying the decorator args, _ = mock_listen_to.call_args return re.compile(*args) @pytest.mark.parametrize( - 'message,expected', + "message,expected", [ # スペースなし - ('name++', True), + ("name++", True), # スペース * 1 - ('name ++', True), + ("name ++", True), # スペース * 2 - ('name ++', True), + ("name ++", True), # @あり - ('@name++', True), + ("@name++", True), # @とスペースあり - ('@name ++', True), + ("@name ++", True), # 複数の名前 - ('name1 name2++', True), + ("name1 name2++", True), # 複数の名前とスペース - ('name1 name2 ++', True), + ("name1 name2 ++", True), # NG: プラスとプラスの間にスペース - ('name+ +', False), + ("name+ +", False), # NG: プラスの後に文字列が続く - ('name++following-message', False), + ("name++following-message", False), # NG: プラスが多い - ('name+++', False), + ("name+++", False), # NG: プラスが多い、スペースあり - ('name +++', False), + ("name +++", False), # NG: さらにプラスが多い - ('name++++', False), - ] + ("name++++", False), + ], ) def test_pattern( - self, target_pattern, + self, + target_pattern, # parameters - message, expected, + message, + expected, ): """パターンにマッチするメッセージを確認する @@ -62,5 +65,5 @@ def test_pattern( actual = target_pattern.match(message) # assert - matched = (actual is not None) + matched = actual is not None assert matched == expected diff --git a/tests/test_plugins_thx.py b/tests/test_plugins_thx.py index 7e9b4ac..72e50b2 100644 --- a/tests/test_plugins_thx.py +++ b/tests/test_plugins_thx.py @@ -9,123 +9,120 @@ class TestFindThx: @pytest.fixture def target(self): from haro.plugins.thx import find_thx + return find_thx @pytest.mark.parametrize( - 'text,expected', + "text,expected", [ # スペースなし ( - 'slack_id++ reason ...', - { - 'reason ...': [('slack_id', 'slack_id')] - }, + "slack_id++ reason ...", + {"reason ...": [("slack_id", "slack_id")]}, ), # スペース * 1 ( - 'slack_id ++ reason ...', - { - 'reason ...': [('slack_id', 'slack_id')] - }, + "slack_id ++ reason ...", + {"reason ...": [("slack_id", "slack_id")]}, ), # スペース * 2 ( - 'slack_id ++ reason ...', - { - 'reason ...': [('slack_id', 'slack_id')] - }, + "slack_id ++ reason ...", + {"reason ...": [("slack_id", "slack_id")]}, ), # @あり ( - '<@slack_id>++ reason ...', - { - 'reason ...': [('slack_id', '<@slack_id>')] - }, + "<@slack_id>++ reason ...", + {"reason ...": [("slack_id", "<@slack_id>")]}, ), # @とスペースあり ( - '<@slack_id> ++ reason ...', - { - 'reason ...': [('slack_id', '<@slack_id>')] - }, + "<@slack_id> ++ reason ...", + {"reason ...": [("slack_id", "<@slack_id>")]}, ), # 複数の名前 ( - 'slack_id1 slack_id2++ reason ...', + "slack_id1 slack_id2++ reason ...", { - 'reason ...': [ - ('slack_id1', 'slack_id1'), - ('slack_id2', 'slack_id2'), + "reason ...": [ + ("slack_id1", "slack_id1"), + ("slack_id2", "slack_id2"), ] }, ), # 複数の名前とスペース ( - 'slack_id1 slack_id2 ++ reason ...', + "slack_id1 slack_id2 ++ reason ...", { - 'reason ...': [ - ('slack_id1', 'slack_id1'), - ('slack_id2', 'slack_id2'), + "reason ...": [ + ("slack_id1", "slack_id1"), + ("slack_id2", "slack_id2"), ] }, ), # 複数行 ( - '\n'.join([ - 'slack_id1++ reason1', - 'slack_id2++ reason2', - ]), + "\n".join( + [ + "slack_id1++ reason1", + "slack_id2++ reason2", + ] + ), { - 'reason1': [('slack_id1', 'slack_id1')], - 'reason2': [('slack_id2', 'slack_id2')], + "reason1": [("slack_id1", "slack_id1")], + "reason2": [("slack_id2", "slack_id2")], }, ), # 複数行 (1行目のプラスがない) ( - '\n'.join([ - 'TO BE IGNORED', # 無視される - 'slack_id++ reason', - ]), + "\n".join( + [ + "TO BE IGNORED", # 無視される + "slack_id++ reason", + ] + ), { - 'reason': [('slack_id', 'slack_id')], + "reason": [("slack_id", "slack_id")], }, ), # 複数行 (2行目のプラスが多い) ( - '\n'.join([ - 'slack_id1++ reason1', - 'slack_id2+++ reason2', # 無視される - ]), + "\n".join( + [ + "slack_id1++ reason1", + "slack_id2+++ reason2", # 無視される + ] + ), { - 'reason1': [('slack_id1', 'slack_id1')], + "reason1": [("slack_id1", "slack_id1")], }, ), # NG: プラスとプラスの間にスペース ( - 'slack_id+ + reason ...', + "slack_id+ + reason ...", {}, ), # NG: プラスの後にスペースがない ( - 'slack_id++reason ...', + "slack_id++reason ...", {}, ), # NG: プラスが多い ( - 'slack_id+++ reason ...', + "slack_id+++ reason ...", {}, ), # NG: プラスが多い、スペースあり ( - 'slack_id +++ reason ...', + "slack_id +++ reason ...", {}, ), # NG: プラスがすごく多い ( - 'slack_id++++ reason ...', + "slack_id++++ reason ...", {}, ), - ] + ], ) def test_find_user_names(self, target, text, expected): """テキストから検出されるユーザーを確認する @@ -137,7 +134,7 @@ def test_find_user_names(self, target, text, expected): """ # act with mock.patch( - 'haro.plugins.thx.get_user_name', + "haro.plugins.thx.get_user_name", lambda x: x, ): actual, _, _ = target(None, text) @@ -147,48 +144,51 @@ def test_find_user_names(self, target, text, expected): class TestUpdateThx: - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def target_pattern(self): - with mock.patch('slackbot.bot.listen_to') as mock_listen_to: + with mock.patch("slackbot.bot.listen_to") as mock_listen_to: from haro.plugins import thx + importlib.reload(thx) # ensure applying the decorator args, _ = mock_listen_to.call_args return re.compile(*args) @pytest.mark.parametrize( - 'message,expected', + "message,expected", [ # スペースなし - ('name++ reason why you are pleased', True), + ("name++ reason why you are pleased", True), # スペース * 1 - ('name ++ reason why you are pleased', True), + ("name ++ reason why you are pleased", True), # スペース * 2 - ('name ++ reason why you are pleased', True), + ("name ++ reason why you are pleased", True), # @あり - ('@name++ reason why you are pleased', True), + ("@name++ reason why you are pleased", True), # @とスペースあり - ('@name ++ reason why you are pleased', True), + ("@name ++ reason why you are pleased", True), # 複数の名前 - ('name1 name2++ reason why you are pleased', True), + ("name1 name2++ reason why you are pleased", True), # 複数の名前とスペース - ('name1 name2 ++ reason why you are pleased', True), + ("name1 name2 ++ reason why you are pleased", True), # NG: プラスとプラスの間にスペース - ('name+ + reason why you are pleased', False), + ("name+ + reason why you are pleased", False), # NG: プラスの後にスペースがない - ('name++reason why you are pleased', False), + ("name++reason why you are pleased", False), # NG: プラスが多い - ('name+++ reason why you are pleased', False), + ("name+++ reason why you are pleased", False), # NG: プラスが多い、スペースあり - ('name +++ reason why you are pleased', False), + ("name +++ reason why you are pleased", False), # NG: プラスがすごく多い - ('name++++ reason why you are pleased', False), - ] + ("name++++ reason why you are pleased", False), + ], ) def test_pattern( - self, target_pattern, + self, + target_pattern, # parameters - message, expected, + message, + expected, ): """パターンにマッチするメッセージを確認する @@ -203,5 +203,5 @@ def test_pattern( actual = target_pattern.match(message) # assert - matched = (actual is not None) + matched = actual is not None assert matched == expected diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 2edc384..a5ffa19 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -17,12 +17,11 @@ @pytest.fixture -def redmine_user(db, user_id="U023BECGF", api_key="d1d567978001e4f884524a8941a9bbe6a8be87ac"): +def redmine_user( + db, user_id="U023BECGF", api_key="d1d567978001e4f884524a8941a9bbe6a8be87ac" +): with db.transaction() as session: - user = RedmineUserFactory.create( - user_id=user_id, - api_key=api_key - ) + user = RedmineUserFactory.create(user_id=user_id, api_key=api_key) session.bulk_save_objects([user]) session.commit() return user @@ -32,8 +31,7 @@ def redmine_user(db, user_id="U023BECGF", api_key="d1d567978001e4f884524a8941a9b def redmine_project(db, project_id=265, channels="C0AGP8QQH,C0AGP8QQZ"): with db.transaction() as session: project_channel = ProjectChannelFactory.create( - project_id=project_id, - channels=channels + project_id=project_id, channels=channels ) session.bulk_save_objects([project_channel]) session.commit() @@ -47,9 +45,7 @@ def slack_message(): channel_mock = Mock() channel_mock._body = {"id": channel, "name": "test_channel"} - configure = { - "channel": channel_mock - } + configure = {"channel": channel_mock} message = MagicMock() message.configure_mock(**configure) @@ -65,9 +61,7 @@ def no_channel_slack_message(): channel_mock = Mock() channel_mock._body = {"id": channel, "name": "test_channel"} - configure = { - "channel": channel_mock - } + configure = {"channel": channel_mock} message = MagicMock() message.configure_mock(**configure) @@ -77,42 +71,57 @@ def no_channel_slack_message(): def test_invalid_user_response(db, slack_message): - with patch('haro.plugins.redmine.Session', lambda: db.session): + with patch("haro.plugins.redmine.Session", lambda: db.session): show_ticket_information(slack_message, "1234567") assert slack_message.send.called is False -def test_no_ticket_permissions_response(db, slack_message, redmine_user, redmine_project): - with patch('haro.plugins.redmine.Session', lambda: db.session): +def test_no_ticket_permissions_response( + db, slack_message, redmine_user, redmine_project +): + with patch("haro.plugins.redmine.Session", lambda: db.session): with requests_mock.mock() as response: ticket_id = "1234567" - url = urljoin(REDMINE_URL, "issues/%s.json?key=%s" % (ticket_id, redmine_user.api_key)) + url = urljoin( + REDMINE_URL, "issues/%s.json?key=%s" % (ticket_id, redmine_user.api_key) + ) response.get(url, status_code=403) show_ticket_information(slack_message, ticket_id) assert slack_message.send.called is True slack_message.send.assert_called_with(RESPONSE_ERROR.format(USER_NAME)) -def test_no_channel_permissions_response(db, slack_message, redmine_user, redmine_project): - with patch('haro.plugins.redmine.Session', lambda: db.session): +def test_no_channel_permissions_response( + db, slack_message, redmine_user, redmine_project +): + with patch("haro.plugins.redmine.Session", lambda: db.session): with requests_mock.mock() as response: ticket_id = "1234567" - channel_name = slack_message.channel._body['name'] + channel_name = slack_message.channel._body["name"] - url = urljoin(REDMINE_URL, "issues/%s.json?key=%s" % (ticket_id, redmine_user.api_key)) + url = urljoin( + REDMINE_URL, "issues/%s.json?key=%s" % (ticket_id, redmine_user.api_key) + ) response.get(url, status_code=200, json={"issue": {"project": {"id": 28}}}) show_ticket_information(slack_message, ticket_id) assert slack_message.send.called is True - slack_message.send.assert_called_with(NO_CHANNEL_PERMISSIONS.format(ticket_id, - channel_name)) + slack_message.send.assert_called_with( + NO_CHANNEL_PERMISSIONS.format(ticket_id, channel_name) + ) def test_successful_response(db, slack_message, redmine_user, redmine_project): - with patch('haro.plugins.redmine.Session', lambda: db.session): - - client, web_api, chat, post_message, res = Mock(), Mock(), Mock(), Mock(), Mock() - res.body = {'ts': 123} + with patch("haro.plugins.redmine.Session", lambda: db.session): + + client, web_api, chat, post_message, res = ( + Mock(), + Mock(), + Mock(), + Mock(), + Mock(), + ) + res.body = {"ts": 123} post_message.return_value = res chat.post_message = post_message web_api.chat = chat @@ -124,32 +133,16 @@ def test_successful_response(db, slack_message, redmine_user, redmine_project): url = urljoin(REDMINE_URL, "issues/%s" % ticket_id) ticket = { - "issue": - { - "id": ticket_id, - "project": - { - "id": redmine_project.project_id - }, - "author": { - "name": "author", - "id": 1 - }, - "subject": "Test Subject", - "description": "Description", - "assigned_to": { - "name": "assigned to", - "id": 1 - }, - "status": { - "name": "status", - "id": 1 - }, - "priority": { - "name": "priority", - "id": 1 - }, - }, + "issue": { + "id": ticket_id, + "project": {"id": redmine_project.project_id}, + "author": {"name": "author", "id": 1}, + "subject": "Test Subject", + "description": "Description", + "assigned_to": {"name": "assigned to", "id": 1}, + "status": {"name": "status", "id": 1}, + "priority": {"name": "priority", "id": 1}, + }, } mock_url = url + ".json?key=%s" % redmine_user.api_key response.get(mock_url, status_code=200, json=ticket) @@ -159,12 +152,16 @@ def test_successful_response(db, slack_message, redmine_user, redmine_project): assert post_message.called is True -def test_no_channels_no_response(db, no_channel_slack_message, redmine_user, redmine_project): - with patch('haro.plugins.redmine.Session', lambda: db.session): +def test_no_channels_no_response( + db, no_channel_slack_message, redmine_user, redmine_project +): + with patch("haro.plugins.redmine.Session", lambda: db.session): with requests_mock.mock() as response: ticket_id = "1234567" - url = urljoin(REDMINE_URL, "issues/%s.json?key=%s" % (ticket_id, redmine_user.api_key)) + url = urljoin( + REDMINE_URL, "issues/%s.json?key=%s" % (ticket_id, redmine_user.api_key) + ) response.get(url, status_code=200, json={"issue": {"project": {"id": 28}}}) show_ticket_information(no_channel_slack_message, ticket_id) diff --git a/tests/test_slack.py b/tests/test_slack.py index 8036123..673217b 100644 --- a/tests/test_slack.py +++ b/tests/test_slack.py @@ -4,98 +4,73 @@ class TestGetUserName: - @pytest.fixture def target(self): from haro.slack import get_user_name + return get_user_name def test_get_user_name(self, target): - """ユーザー名が返る事 - """ - user_id, name = 'U41NH7LFJ', 'haro' - user_info = { - user_id: { - 'name': name, - 'display_name': 'HARO' - } - } - - with patch('haro.slack.get_users_info', return_value=user_info): + """ユーザー名が返る事""" + user_id, name = "U41NH7LFJ", "haro" + user_info = {user_id: {"name": name, "display_name": "HARO"}} + + with patch("haro.slack.get_users_info", return_value=user_info): actual = target(user_id) assert name == actual def test_get_user_name_with_none(self, target): - """存在しないユーザー名であればNoneが返る事 - """ - user_id = 'U41NH7LFJ' + """存在しないユーザー名であればNoneが返る事""" + user_id = "U41NH7LFJ" - with patch('haro.slack.get_users_info', return_value={}): + with patch("haro.slack.get_users_info", return_value={}): actual = target(user_id) assert None is actual class TestGetUserDisplayName: - @pytest.fixture def target(self): from haro.slack import get_user_display_name + return get_user_display_name def test_get_user_display_name(self, target): - """表示名が返る事 - """ - user_id, display_name = 'U41NH7LFJ', 'HARO' - user_info = { - user_id: { - 'name': 'haro', - 'display_name': display_name - } - } - - with patch('haro.slack.get_users_info', return_value=user_info): + """表示名が返る事""" + user_id, display_name = "U41NH7LFJ", "HARO" + user_info = {user_id: {"name": "haro", "display_name": display_name}} + + with patch("haro.slack.get_users_info", return_value=user_info): actual = target(user_id) assert display_name == actual def test_get_user_display_name_with_empty(self, target): - """表示名がなければユーザー名が返る事 - """ - user_id, name = 'U41NH7LFJ', 'haro' - user_info = { - user_id: { - 'name': name, - 'display_name': '' - } - } - - with patch('haro.slack.get_users_info', return_value=user_info): + """表示名がなければユーザー名が返る事""" + user_id, name = "U41NH7LFJ", "haro" + user_info = {user_id: {"name": name, "display_name": ""}} + + with patch("haro.slack.get_users_info", return_value=user_info): actual = target(user_id) assert name == actual class TestGetSlackIdByName: - @pytest.fixture def target(self): from haro.slack import get_slack_id_by_name + return get_slack_id_by_name def test_get_slack_id_by_name(self, target): - """ユーザー名からユーザーIDが取得できる事 - """ - user_id, name = 'U41NH7LFJ', 'haro' - user_info = { - user_id: { - 'name': name, - 'display_name': '' - } - } - - with patch('haro.slack.get_users_info', return_value=user_info): + """ユーザー名からユーザーIDが取得できる事""" + user_id, name = "U41NH7LFJ", "haro" + user_info = {user_id: {"name": name, "display_name": ""}} + + with patch("haro.slack.get_users_info", return_value=user_info): actual = target(name) assert user_id == actual diff --git a/tests/test_version.py b/tests/test_version.py index bc1066f..d42e510 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,55 +1,57 @@ from unittest import mock import pytest + class TestVersion: @pytest.fixture def target(self): from haro.plugins.version import version + return version @pytest.mark.parametrize( - 'change_log,expected', + "change_log,expected", [ # issue番号が数値 ( # ChangeLog.rst - 'Unreleased\n' + \ - '----------\n' + \ - '\n' + \ - 'Release Notes - 2020-10-30\n' + \ - '--------------------------\n' + \ - '- [#210] READMEとenv.sampleにREDMINE_API_KEYについて追記\n' + \ - '\n' + \ - 'Release Notes - 2020-10-23\n' + \ - '--------------------------\n' + \ - '- [#207] Redmine Reminderが動いていないバグ\n', + "Unreleased\n" + + "----------\n" + + "\n" + + "Release Notes - 2020-10-30\n" + + "--------------------------\n" + + "- [#210] READMEとenv.sampleにREDMINE_API_KEYについて追記\n" + + "\n" + + "Release Notes - 2020-10-23\n" + + "--------------------------\n" + + "- [#207] Redmine Reminderが動いていないバグ\n", # expected - 'Release Notes - 2020-10-30\n' + \ - '--------------------------\n' + \ - '- [#210] READMEとenv.sampleにREDMINE_API_KEYについて追記\n' + "Release Notes - 2020-10-30\n" + + "--------------------------\n" + + "- [#210] READMEとenv.sampleにREDMINE_API_KEYについて追記\n", ), # issue番号が数値以外 - ( + ( # ChangeLog.rst - 'Unreleased\n' + \ - '----------\n' + \ - '\n' + \ - 'Release Notes - 2020-10-30\n' + \ - '--------------------------\n' + \ - '- [#foo] READMEとenv.sampleにREDMINE_API_KEYについて追記\n' + \ - '\n' + \ - 'Release Notes - 2020-10-23\n' + \ - '--------------------------\n' + \ - '- [#bar] Redmine Reminderが動いていないバグ\n', + "Unreleased\n" + + "----------\n" + + "\n" + + "Release Notes - 2020-10-30\n" + + "--------------------------\n" + + "- [#foo] READMEとenv.sampleにREDMINE_API_KEYについて追記\n" + + "\n" + + "Release Notes - 2020-10-23\n" + + "--------------------------\n" + + "- [#bar] Redmine Reminderが動いていないバグ\n", # expected - 'Release Notes - 2020-10-30\n' + \ - '--------------------------\n' + \ - '- [#foo] READMEとenv.sampleにREDMINE_API_KEYについて追記\n' - ) - ] + "Release Notes - 2020-10-30\n" + + "--------------------------\n" + + "- [#foo] READMEとenv.sampleにREDMINE_API_KEYについて追記\n", + ), + ], ) def test_get_version_success(self, target, change_log, expected): - with mock.patch('haro.plugins.version.read_change_log') as m: + with mock.patch("haro.plugins.version.read_change_log") as m: # arrange m.return_value = change_log # act @@ -58,10 +60,10 @@ def test_get_version_success(self, target, change_log, expected): assert actual == expected def test_get_version_failed(self, target): - with mock.patch('haro.plugins.version.read_change_log') as m: + with mock.patch("haro.plugins.version.read_change_log") as m: # arrange m.side_effect = FileNotFoundError - expected = 'リリースノートが見つかりません' + expected = "リリースノートが見つかりません" # act actual = target() # assert diff --git a/tox.ini b/tox.ini index ad00141..bdcfac5 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ commands = pytest {posargs} commands = pytest {posargs} \ --junitxml={toxinidir}/test-results/pytest.xml -[testenv:flake8] +[testenv:lint] deps = flake8 flake8-blind-except @@ -34,17 +34,28 @@ deps = flake8_polyfill mccabe radon + black basepython = python3.8 -commands = flake8 src/haro +commands = + flake8 src/haro + black --check . -[testenv:flake8_ci] +[testenv:fmt] +deps = + black +basepython = python3.8 +commands = black . + +[testenv:lint_ci] deps = - {[testenv:flake8]deps} + {[testenv:lint]deps} flake8_formatter_junit_xml basepython = python3.8 -commands = flake8 src/haro --output-file={toxinidir}/test-results/flake8.xml --format junit-xml +commands = + flake8 src/haro --output-file={toxinidir}/test-results/flake8.xml --format junit-xml + black --check . [pytest] testpaths = tests @@ -61,4 +72,8 @@ ignore = # I100: Import statements are in the wrong order. I100, # I101: Imported names are in the wrong order. - I101 + I101, + # W503 line break before binary operator. + W503, + # E203 whitespace before ':' + E203