Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for slack #141

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/meticulous/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,25 @@
@click.version_option(version=__version__)
@click.option("--target", nargs=1)
@click.option("--start/--no-start", default=False)
@click.option("--slack/--no-slack", default=False)
@click.pass_context
def main(ctxt, target, start):
def main(ctxt, target, start, slack):
"""
Main click group handler
"""
if ctxt.invoked_subcommand is None:
run_invocation(target, start)
run_invocation(target, start, slack)


@main.command()
@click.option("--target", nargs=1)
@click.option("--start/--no-start", default=False)
def invoke(target, start):
@click.option("--slack/--no-slack", default=False)
def invoke(target, start, slack):
"""
Primary command handler
"""
run_invocation(target, start)
run_invocation(target, start, slack)


@main.command()
Expand Down
2 changes: 1 addition & 1 deletion app/meticulous/_addrepo.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def update_json_results(repo, words):
items = list(words.items())
random.SystemRandom().shuffle(items)
count = 0
max_suggestions = 50
max_suggestions = 15
for index, (word, details) in enumerate(items):
add_progress(key, f"Processing {index + 1} of {len(items)} for {repo}")
if unanimous.util.is_nonword(word):
Expand Down
Empty file added app/meticulous/_corestate.py
Empty file.
7 changes: 5 additions & 2 deletions app/meticulous/_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
)
from meticulous._util import get_editor
from meticulous._webserver import main as webserver_main
from meticulous._slackmode import main as slackmode_main


def get_spelling_store_path(target):
Expand All @@ -50,7 +51,7 @@ def get_spelling_store_path(target):
return path / "spelling.db"


def run_invocation(target, start):
def run_invocation(target, start, slack_mode):
"""
Execute the invocation
"""
Expand All @@ -66,7 +67,9 @@ def run_invocation(target, start):
load_recent_non_words(target)
validate_versions()
try:
if start or get_confirmation("Run webserver?"):
if slack_mode:
slackmode_main(target)
elif start or get_confirmation("Run webserver?"):
webserver_main(target, start)
elif get_confirmation("Run automated multi-queue processing?"):
multiworker_main(target)
Expand Down
10 changes: 7 additions & 3 deletions app/meticulous/_processrepo.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,12 @@ def interactive_task_collect_nonwords( # pylint: disable=unused-argument
if not jsonpath.is_file():
interaction.send(f"Unable to locate spelling at {jsonpath}")
return
with io.open(jsonpath, "r", encoding="utf-8") as fobj:
jsonobj = json.load(fobj)
try:
with io.open(jsonpath, "r", encoding="utf-8") as fobj:
jsonobj = json.load(fobj)
except json.decoder.JSONDecodeError:
interaction.send(f"Unable to read spelling at {jsonpath}")
return
state = NonwordState(
interaction=interaction,
target=target,
Expand Down Expand Up @@ -357,7 +361,7 @@ def get_sorted_words(interaction, jsonobj):
order.append(((priority, len(details["files"]), replacement), word))
order.sort(reverse=True)
interaction.send(f"-- Candidates Found: {len(order)} --")
maxwords = 50
maxwords = 15
wordchoice = []
for num, ((priority, num_files, replacement), word) in enumerate(order[:maxwords]):
if not replacement:
Expand Down
248 changes: 248 additions & 0 deletions app/meticulous/_slackmode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
"""
Alternative way of running meticulous via slack conversations
"""

import datetime
import logging
import os
import re
from threading import Condition

from slack_sdk.rtm_v2 import RTMClient
from slack_sdk import WebClient

from meticulous._multiworker import Interaction, multiworker_core

INPUT = 0
CONFIRMATION = 1


class SlackStateHandler(Interaction):
"""
Records the state to await user responses
"""

def __init__(self):
self.alive = False
self.started_at = datetime.datetime.min
self.condition = Condition()
self.await_key = None
self.response_val = None
self.messages = []

def join_messages(self, message):
self.messages.append(message)
message = "\n".join(self.messages)
del self.messages[:]
return message

def run(self, target):
"""
Perform processing
"""
self.started_at = datetime.datetime.now()
self.alive = True
while self.alive:
multiworker_core(self, target)

def stop(self):
"""
Gracefully stop processing
"""
self.alive = False
self.started_at = datetime.datetime.min

def get_input(self, message):
message = self.join_messages(message)
return self.get_await(Input(message))

def make_choice(self, choices, message="Please make a selection."):
message = self.join_messages(message)
return self.get_await(Choice(choices, message))

def check_quit(self, controller):
return controller.tasks_empty()

def get_confirmation(self, message="Do you want to continue", defaultval=True):
message = self.join_messages(message)
return self.get_await(Confirmation(message, defaultval))

def get_await(self, key):
"""
Work out the user response
"""
with self.condition:
self.response_val = None
self.await_key = key
while self.response_val is None:
self.condition.wait(10)
return self.response_val

def receive(self, message):
if self.await_key is None:
MESSAGES.send_message("Message discarded {message!r} not ready yet.")
return
self.await_key.handle(self, message)

def send(self, message):
self.messages.append(message)

def respond(self, val):
"""
A response is chosen
"""
with self.condition:
self.await_key = None
self.response_val = val
self.condition.notify()


class Awaiter:
"""
Waiting on some user input
"""

def __init__(self):
pass

def handle(self, state, message):
"""
Handle slack reposnse
"""
raise NotImplementedError()


class Confirmation(Awaiter):
"""
Yes/No
"""

def __init__(self, message, defaultval):
super().__init__()
self.defaultval = defaultval
MESSAGES.send_message(message)

def handle(self, state, message):
"""
Handle reply
"""
umessage = message.upper()
if umessage not in ("Y", "N"):
MESSAGES.send_message(f"Unknown response {message!r} is not Y or N")
return
state.respond(umessage == "Y")


class Input(Awaiter):
"""
Text Input
"""

def __init__(self, message):
super().__init__()
MESSAGES.send_message(message)

def handle(self, state, message):
"""
Handle reply
"""
state.respond(message)


class Choice(Awaiter):
"""
Selection from choices
"""

def __init__(self, choices, message):
super().__init__()
options = list(enumerate(sorted(choices.keys())))
self.choices = {index: choices[txt] for index, txt in options}
option_message = "\n".join(
f"{index}. {txt}"
for index, txt in (
(indx, text.split(") ", 1)[-1]) for indx, text in options
)
)
MESSAGES.send_message(f"{message}\n\n\n{option_message}")

def handle(self, state, message):
"""
Handle reply
"""
try:
state.respond(self.choices[int(message)])
except (IndexError, ValueError, KeyError):
MESSAGES.send_message(f"Unknown response {message!r} not in choices")
return


class SlackMessageHandler:
def __init__(self):
self.client = None
self.rtm_client = None
self.channel = None

def start(self):
slack_token = os.environ["SLACK_METICULOUS_TOKEN"]
self.channel = os.environ["SLACK_METICULOUS_CHANNEL"]
self.client = WebClient(token=slack_token)
self.rtm_client = RTMClient(token=slack_token)

@self.rtm_client.on("message")
def handler(client, event):
if event.get("bot_id"):
return
event_text = event.get("text")
if event_text:
STATE.receive(event_text)

self.rtm_client.connect()
self.send_message("Meticulous started.")

def send_message(self, text):
if not text:
return
self.client.api_call(
"chat.postMessage",
params={
"channel": self.channel,
"as_user": True,
"text": "\n".join(
self.replace_ansi(self.replace_slack_formatting(line.rstrip("\n")))
for line in text.splitlines()
),
},
)

@staticmethod
def replace_slack_formatting(line):
re1 = re.compile(r"[*_~`]")
for r in [re1]:
line = r.sub(".", line)
return line

@staticmethod
def replace_ansi(line):
re1 = re.compile(r"\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]")
re2 = re.compile(r"\x1b[PX^_].*?\x1b\\")
re3 = re.compile(r"\x1b\][^\a]*(?:\a|\x1b\\)")
re4 = re.compile(r"\x1b[\[\]A-Z\\^_@]")
re5 = re.compile(r"[\x00-\x1f\x7f-\x9f\xad]+")
for r in [re1, re2, re3, re4, re5]:
line = r.sub("*", line)
return line


STATE = SlackStateHandler()
MESSAGES = SlackMessageHandler()


def main(target):
"""
Alternative way of running meticulous via slack conversations
"""
logging.basicConfig(level=logging.WARNING)
logging.debug("running slack...")
MESSAGES.start()
STATE.run(target)
4 changes: 2 additions & 2 deletions app/meticulous/_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ def push_commit_multi(repodir, branch_name):
os.chdir(pathlib.Path.home())
with local.cwd(repodir):
to_branch = git("symbolic-ref", "--short", "HEAD").strip()
git("commit", "-F", "__commit__.txt")
git("commit", "-s", "-F", "__commit__.txt")
git("push", "origin", f"{to_branch}:{branch_name}")
return to_branch

Expand All @@ -615,7 +615,7 @@ def amend_commit(repository_saves_multi, from_branch, to_branch):
# plumbum bug workaround
os.chdir(pathlib.Path.home())
with local.cwd(repodir):
git("commit", "-F", "__commit__.txt", "--amend")
git("commit", "-s", "-F", "__commit__.txt", "--amend")
git("push", "origin", "-f", f"{to_branch}:{from_branch}")


Expand Down