diff --git a/.githooks/commit-msg b/.githooks/commit-msg deleted file mode 100644 index 5e46509..0000000 --- a/.githooks/commit-msg +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -MSG="$1" - -if ! grep -qE "^(bfix|chor|conf|docs|feat|ptch|rfac): " "$MSG";then - echo "Incorrect commit message." - echo "Start your message with either" - echo " - 'feat: '" - echo " - 'ptch: '" - echo " - 'bfix: '" - echo " - 'rfac: '" - echo " - 'chor: '" - echo " - 'docs: '" - echo " - 'conf: '" - exit 1 -fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100644 index 92b1f93..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -. venv/Scripts/activate - -# If no changes, exit cleanly. -changed=$(git diff --cached --name-only) -if [[ -z "$changed" ]]; then - exit 0 -fi - -# If merge markers in changelist, throw error. -echo "$changed" | xargs egrep '^[><=]{7}( |$)' -H -I --line-number -if [ $? == 0 ]; then - echo "WARNING: You have merge markers in the above files. Fix them before committing." - exit 1 -fi - -# If wrongly formatted Python files in directory, throw error. -black --check . -if [ $? == 1 ]; then - echo "WARNING: Some Python files in the working directory are not properly formatted." - exit 1 -fi - -deactivate diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f917096..0899738 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,17 +7,28 @@ jobs: runs-on: ubuntu-latest steps: + # Setup - name: Checkout code uses: actions/checkout@v2 + - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.10" + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - - name: Lint with black + + # Lint + - name: Lint with Black run: | pip install black black --check . + + - name: Lint with Pylint + run: | + pip install pylint + touch __init__.py + pylint `pwd` --fail-under=5 --disable=too-few-public-methods,fixme diff --git a/.gitignore b/.gitignore index e9dc610..035836a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env .idea venv -**__pycache__** +*.pyc +__init__.py diff --git a/README.md b/README.md index c9ea6b0..77a8294 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,10 @@ $ pip install -r requirements.txt ``` Or, if your system contains both Python 2 and Python 3, use `pip3`. -#### 5. Enable Git hooks - -``` -$ git config --local --add core.hooksPath .githooks -``` - -#### 6. Get the environment variables from another contributor -#### 7. [Download and configure ngrok](https://betterprogramming.pub/ngrok-make-your-localhost-accessible-to-anyone-333b99e44b07) -#### 8. Send your generated URL to MDG org admins -#### 9. Run the bot +#### 5. Get the environment variables from another contributor +#### 6. [Download and configure ngrok](https://betterprogramming.pub/ngrok-make-your-localhost-accessible-to-anyone-333b99e44b07) +#### 7. Send your generated URL to MDG org admins +#### 8. Run the bot ``` $ python main.py diff --git a/github_parsers.py b/github_parsers.py index 5959b97..9bdbde3 100644 --- a/github_parsers.py +++ b/github_parsers.py @@ -8,7 +8,7 @@ class GitHubPayloadParser: @staticmethod - def parse(event_type, raw_json) -> GitHubEvent: + def parse(event_type, raw_json) -> GitHubEvent | None: json: JSON = JSON(raw_json) event_parsers: list[Type[EventParser]] = [ BranchCreateEventParser, @@ -37,6 +37,8 @@ def parse(event_type, raw_json) -> GitHubEvent: event_type=event_type, json=json, ) + print(f"Undefined event: {raw_json}") + return None # Helper classes: diff --git a/main.py b/main.py index dc7719a..92ee37d 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,3 @@ -from typing import Optional - from bottle import post, run, request, get from github_parsers import GitHubPayloadParser @@ -22,18 +20,18 @@ def test_post(): @post("/github/events") def manage_github_events(): - event: GitHubEvent = GitHubPayloadParser.parse( + event: GitHubEvent | None = GitHubPayloadParser.parse( event_type=request.headers["X-GitHub-Event"], raw_json=request.json, ) - bot.inform(event) + if event is not None: + bot.inform(event) @post("/slack/commands") -def manage_slack_commands(): - response: Optional[dict] = bot.run(raw_json=request.forms) - if response is not None: - return response +def manage_slack_commands() -> dict | None: + response: dict | None = bot.run(raw_json=request.forms) + return response bot: SlackBot = SlackBot() diff --git a/models/github.py b/models/github.py index 3e098f6..f090556 100644 --- a/models/github.py +++ b/models/github.py @@ -1,5 +1,5 @@ import enum -from typing import Optional, Literal +from typing import Literal from models.link import Link @@ -7,9 +7,9 @@ class Commit: def __init__( self, - message: Optional[str] = None, - sha: Optional[str] = None, - link: Optional[str] = None, + message: str | None = None, + sha: str | None = None, + link: str | None = None, ): self.message = message self.sha = sha @@ -55,14 +55,14 @@ def __init__( **kwargs, ): self.name = name - self.link: Optional[str] = kwargs.get("link", None) + self.link: str | None = kwargs.get("link", None) self.type = ref_type class Repository: def __init__(self, name: str, **kwargs): self.name = name - self.link: Optional[str] = kwargs.get("link", None) + self.link: str | None = kwargs.get("link", None) class User: @@ -71,21 +71,22 @@ def __init__(self, name: str, **kwargs): self.link = kwargs.get("link", f"https://github.com/{name}") +# pylint: disable-next=too-many-instance-attributes class GitHubEvent: def __init__(self, event_type: EventType, repo: Repository, **kwargs): self.type = event_type self.repo = repo - self.number: Optional[int] = kwargs.get("number", None) + self.number: int | None = kwargs.get("number", None) - self.status: Optional[str] = kwargs.get("status", None) - self.title: Optional[str] = kwargs.get("title", None) + self.status: str | None = kwargs.get("status", None) + self.title: str | None = kwargs.get("title", None) - self.branch: Optional[Ref] = kwargs.get("branch", None) - self.user: Optional[User] = kwargs.get("user", None) + self.branch: Ref | None = kwargs.get("branch", None) + self.user: User | None = kwargs.get("user", None) - self.comments: Optional[list[str]] = kwargs.get("comments", None) + self.comments: list[str] | None = kwargs.get("comments", None) - self.commits: Optional[list[Commit]] = kwargs.get("commits", None) - self.links: Optional[list[Link]] = kwargs.get("links", None) - self.reviewers: Optional[list[User]] = kwargs.get("reviewers", None) + self.commits: list[Commit] | None = kwargs.get("commits", None) + self.links: list[Link] | None = kwargs.get("links", None) + self.reviewers: list[User] | None = kwargs.get("reviewers", None) diff --git a/models/link.py b/models/link.py index 28923e5..401431c 100644 --- a/models/link.py +++ b/models/link.py @@ -1,8 +1,5 @@ -from typing import Optional - - class Link: - def __init__(self, url: Optional[str] = None, text: Optional[str] = None): + def __init__(self, url: str | None = None, text: str | None = None): self.url = url self.text = text diff --git a/slack_bot.py b/slack_bot.py index 5438d21..b65ada9 100644 --- a/slack_bot.py +++ b/slack_bot.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Optional +from typing import Any from bottle import MultiDict from dotenv import load_dotenv @@ -57,21 +57,23 @@ def inform(self, event: GitHubEvent) -> None: def calculate_channels(self, repo: str, event_type: EventType) -> list[str]: if repo not in self.subscriptions: return [] - else: - correct_channels: list[str] = [] - for channel in self.subscriptions[repo]: - if channel.is_subscribed_to(event=event_type): - correct_channels += [channel.name] - return correct_channels + correct_channels: list[str] = [] + for channel in self.subscriptions[repo]: + if channel.is_subscribed_to(event=event_type): + correct_channels += [channel.name] + return correct_channels @staticmethod - def compose_message(event: GitHubEvent) -> tuple[str, Optional[str]]: + def compose_message(event: GitHubEvent) -> tuple[str, str | None]: message: str = "" - details: Optional[str] = None + details: str | None = None # TODO: Beautify messages. if event.type == EventType.branch_created: - message = f"{event.repo.name}::\tBranch created by {event.user.name}: `{event.branch.name}`." + message = ( + f"{event.repo.name}::\t" + f"Branch created by {event.user.name}: `{event.branch.name}`." + ) elif event.type == EventType.issue_opened: message = ( f"{event.repo.name}::\t" @@ -92,9 +94,17 @@ def compose_message(event: GitHubEvent) -> tuple[str, Optional[str]]: ) elif event.type == EventType.push: if len(event.commits) == 1: - message = f"{event.user.name} pushed to {event.branch.name}, one new commit:\n>{event.commits[0]}" + message = ( + f"{event.user.name} pushed to " + f"{event.branch.name}," + f" one new commit:\n>{event.commits[0]}" + ) else: - message = f"{event.user.name} pushed to {event.branch.name}, {len(event.commits)} new commits:" + message = ( + f"{event.user.name} pushed to " + f"{event.branch.name}, " + f"{len(event.commits)} new commits:" + ) for i, commit in enumerate(event.commits): message += f"\n>{i}. {commit.message}" elif event.type == EventType.review: @@ -106,7 +116,7 @@ def compose_message(event: GitHubEvent) -> tuple[str, Optional[str]]: return message, details - def send_message(self, channel: str, message: str, details: Optional[str]) -> None: + def send_message(self, channel: str, message: str, details: str | None) -> None: if details is None: print(f"Sending {message} to {channel}") self.client.chat_postMessage(channel=channel, text=message) @@ -120,157 +130,186 @@ def send_message(self, channel: str, message: str, details: Optional[str]) -> No ) # Slash commands related methods - def run(self, raw_json: MultiDict) -> Optional[dict]: + def run(self, raw_json: MultiDict) -> str | None: json: JSON = JSON.from_multi_dict(raw_json) current_channel: str = "#" + json["channel_name"] command: str = json["command"] args: list[str] = json["text"].split() - repo: Optional[str] = args[0] if len(args) > 0 else None - if command == "/subscribe": - new_events: set[EventType] = { - SlackBot.convert_str_to_event_type(arg) for arg in args[1:] - } - if repo in self.subscriptions: - channels: set[Channel] = self.subscriptions[repo] - channel: Optional[Channel] = None - for subscribed_channel in channels: - if subscribed_channel.name == current_channel: - channel = subscribed_channel - if channel is None: - # If this channel has not subscribed to any events - # from this repo, add a subscription. - channels.add( - Channel( - name=current_channel, - events=new_events, - ) - ) - self.subscriptions[repo] = channels - else: - # If this channel has subscribed to some events - # from this repo, update the list of events. - old_events: set[EventType] = channel.events - self.subscriptions[repo].remove(channel) - self.subscriptions[repo].add( - Channel( - name=current_channel, - events=(old_events.union(new_events)), - ) - ) - else: - # If no one has subscribed to this repo, add a repo entry. - self.subscriptions[repo] = { + if command == "/subscribe" and len(args) > 0: + self.run_subscribe_command(current_channel=current_channel, args=args) + elif command == "/unsubscribe" and len(args) > 0: + self.run_unsubscribe_command(current_channel=current_channel, args=args) + elif command == "/list": + return self.run_list_command(current_channel=current_channel) + elif command == "/help": + return self.run_help_command() + return None + + def run_subscribe_command(self, current_channel: str, args: list[str]): + repo: [str] = args[0] + new_events: set[EventType] = { + SlackBot.convert_str_to_event_type(arg) for arg in args[1:] + } + # Remove all the entries which do not correspond to a correct [EventType]. + new_events -= [None] + if repo in self.subscriptions: + channels: set[Channel] = self.subscriptions[repo] + channel: Channel | None = next( + ( + subscribed_channel + for subscribed_channel in channels + if subscribed_channel.name == current_channel + ), + None, + ) + if channel is None: + # If this channel has not subscribed to any events + # from this repo, add a subscription. + channels.add( Channel( name=current_channel, events=new_events, ) - } - elif command == "/unsubscribe" and repo in self.subscriptions: - channels: set[Channel] = self.subscriptions[repo] - channel: Optional[Channel] = None - for subscribed_channel in channels: - if subscribed_channel.name == current_channel: - channel = subscribed_channel - if channel is not None: + ) + self.subscriptions[repo] = channels + else: # If this channel has subscribed to some events # from this repo, update the list of events. - events = channel.events - for arg in args[1:]: - event: EventType = SlackBot.convert_str_to_event_type(arg) - try: - events.remove(event) - except KeyError: - # This means that the user tried to unsubscribe from - # an event that wasn't subscribed to in the first place. - pass + old_events: set[EventType] = channel.events self.subscriptions[repo].remove(channel) - if len(events) != 0: - self.subscriptions[repo].add( - Channel( - name=current_channel, - events=events, - ) + self.subscriptions[repo].add( + Channel( + name=current_channel, + events=(old_events.union(new_events)), ) - elif command == "/list": - blocks: list[dict] = [] - for repo in self.subscriptions.keys(): - channels: set[Channel] = self.subscriptions[repo] - channel: Optional[Channel] = None - for subscribed_channel in channels: - if subscribed_channel.name == current_channel: - channel = subscribed_channel - if channel is None: - continue - events_string = ", ".join(event.name for event in channel.events) - blocks += [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"*{repo}*\n{events_string}", - }, - }, - { - "type": "divider", - }, - ] - return { - "response_type": "in_channel", - "blocks": blocks, + ) + else: + # If no one has subscribed to this repo, add a repo entry. + self.subscriptions[repo] = { + Channel( + name=current_channel, + events=new_events, + ) } - elif command == "/help": - # TODO: Prettify events section. - return { - "response_type": "ephemeral", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ( - "*Commands*\n" - "1. `/subscribe [ ...]`\n" - "2. `/unsubsribe [ ...]`\n" - "3. `/list`\n" - "4. `/help`" - ), - }, + + def run_unsubscribe_command(self, current_channel: str, args: list[str]): + repo: [str] = args[0] + channels: set[Channel] = self.subscriptions[repo] + channel: Channel | None = next( + ( + subscribed_channel + for subscribed_channel in channels + if subscribed_channel.name == current_channel + ), + None, + ) + if channel is not None: + # If this channel has subscribed to some events + # from this repo, update the list of events. + events = channel.events + for arg in args[1:]: + event: EventType | None = SlackBot.convert_str_to_event_type(arg) + try: + events.remove(event) + except KeyError: + # This means that the user tried to unsubscribe from + # an event that wasn't subscribed to in the first place. + pass + self.subscriptions[repo].remove(channel) + if len(events) != 0: + self.subscriptions[repo].add( + Channel( + name=current_channel, + events=events, + ) + ) + + def run_list_command(self, current_channel: str) -> dict[str, Any]: + blocks: list[dict] = [] + for repo, channels in self.subscriptions.items(): + channel: Channel | None = next( + ( + subscribed_channel + for subscribed_channel in channels + if subscribed_channel.name == current_channel + ), + None, + ) + if channel is None: + continue + events_string = ", ".join(event.name for event in channel.events) + blocks += [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*{repo}*\n{events_string}", }, - {"type": "divider"}, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ( - "*Events*\n" - "GitHub events are abbreviated as follows:\n" - "1. `bc`: branch_created\n" - "2. `bd`: branch_deleted\n" - "3. `tc`: tag_created\n" - "4. `td`: tag_deleted\n" - "5. `prc`: pull_closed\n" - "6. `prm`: pull_merged\n" - "7. `pro`: pull_opened\n" - "8. `prr`: pull_ready\n" - "9. `io`: issue_opened\n" - "10. `ic`: issue_closed\n" - "11. `rv`: review\n" - "12. `rc`: review_comment\n" - "13. `cc`: commit_comment\n" - "14. `fk`: fork\n" - "15. `p`: push\n" - "16. `rl`: release\n" - "17. `sa`: star_added\n" - "18. `sr`: star_removed\n" - ), - }, + }, + { + "type": "divider", + }, + ] + return { + "response_type": "in_channel", + "blocks": blocks, + } + + @staticmethod + def run_help_command() -> dict[str, Any]: + # TODO: Prettify events section. + return { + "response_type": "ephemeral", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Commands*\n" + "1. `/subscribe [ ...]`\n" + "2. `/unsubsribe [ ...]`\n" + "3. `/list`\n" + "4. `/help`" + ), }, - ], - } - return + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Events*\n" + "GitHub events are abbreviated as follows:\n" + "1. `bc`: branch_created\n" + "2. `bd`: branch_deleted\n" + "3. `tc`: tag_created\n" + "4. `td`: tag_deleted\n" + "5. `prc`: pull_closed\n" + "6. `prm`: pull_merged\n" + "7. `pro`: pull_opened\n" + "8. `prr`: pull_ready\n" + "9. `io`: issue_opened\n" + "10. `ic`: issue_closed\n" + "11. `rv`: review\n" + "12. `rc`: review_comment\n" + "13. `cc`: commit_comment\n" + "14. `fk`: fork\n" + "15. `p`: push\n" + "16. `rl`: release\n" + "17. `sa`: star_added\n" + "18. `sr`: star_removed\n" + ), + }, + }, + ], + } @staticmethod - def convert_str_to_event_type(event_name: str) -> EventType: + def convert_str_to_event_type(event_name: str) -> EventType | None: for event_type in EventType: if event_type.value == event_name: return event_type + print("Event not in enum") + return None diff --git a/utils.py b/utils.py index 209827b..c170e1d 100644 --- a/utils.py +++ b/utils.py @@ -29,8 +29,7 @@ def get(k): for key in keys: if key in self.data: return get(key) - else: - return keys[0].upper() + return keys[0].upper() @staticmethod def from_multi_dict(multi_dict: MultiDict):